Files
wordclock/src/wordclock_esp8266.cpp

1081 lines
36 KiB
C++

/**
* Wordclock 2.0 - Wordclock with ESP8266 and NTP time update
*
* created by techniccontroller 04.12.2021
*
* components:
* - ESP8266
* - Neopixelstrip
*
* Board settings:
* - Board: NodeMCU 1.0 (ESP-12E Module)
* - Flash Size: 4MB (FS:2MB OTA:~1019KB)
* - Upload Speed: 115200
*
*
* with code parts from:
* - Adafruit NeoPixel strandtest.ino, https://github.com/adafruit/Adafruit_NeoPixel/blob/master/examples/strandtest/strandtest.ino
* - Esp8266 und Esp32 webserver https://fipsok.de/
* - https://github.com/pmerlin/PMR-LED-Table/blob/master/tetrisGame.ino
* - https://randomnerdtutorials.com/wifimanager-with-esp8266-autoconnect-custom-parameter-and-manage-your-ssid-and-password/
*
*/
#include <Arduino.h>
#include "wordclock_esp8266.h"
#include <Adafruit_GFX.h> // https://github.com/adafruit/Adafruit-GFX-Library
#include <Adafruit_NeoMatrix.h> // https://github.com/adafruit/Adafruit_NeoMatrix
#include <Adafruit_NeoPixel.h> // NeoPixel library used to run the NeoPixel LEDs: https://github.com/adafruit/Adafruit_NeoPixel
#include <base64.hpp>
#include <EEPROM.h> // from ESP8266 Arduino Core (automatically installed when ESP8266 was installed via Boardmanager)
#include <ESP8266_ISR_Timer.h> // https://github.com/khoih-prog/ESP8266TimerInterrupt
#include <ESP8266TimerInterrupt.h> // https://github.com/khoih-prog/ESP8266TimerInterrupt
#include <ESP8266WebServer.h>
#include <ESP8266WiFi.h>
#include <WiFiManager.h> // https://github.com/tzapu/WiFiManager WiFi Configuration Magic
// own libraries
#include "animation_functions.h"
#include "led_matrix.h"
#include "littlefs_wrapper.h"
#include "ota_functions.h"
#include "pong.h"
#include "render_functions.h"
#include "snake.h"
#include "tetris.h"
#include "time_manager.h"
#include "udp_logger.h"
#include "wordclock_constants.h"
// ----------------------------------------------------------------------------------
// GLOBAL VARIABLES
// ----------------------------------------------------------------------------------
UDPLogger logger; // Global UDP logger instance
Adafruit_NeoMatrix matrix = Adafruit_NeoMatrix(MATRIX_WIDTH, MATRIX_HEIGHT + 1, NEOPIXEL_PIN,
NEO_MATRIX_TOP + NEO_MATRIX_LEFT + NEO_MATRIX_ROWS + NEO_MATRIX_ZIGZAG,
NEO_GRB + NEO_KHZ800); // NeoMatrix
LEDMatrix led_matrix = LEDMatrix(&matrix, DEFAULT_BRIGHTNESS, &logger); // NeoMatrix wrapper
ESP8266WebServer webserver(HTTP_PORT); // Webserver
// ----------------------------------------------------------------------------------
// STATIC VARIABLES
// ----------------------------------------------------------------------------------
// EEPROM values
static EepromLayout_st eeprom_buffer = {{0, 0, 0, 0}, {0U, 0U, 0U, false}, {0U, 0U, 0U, 0U}};
static Brightness_st *const brightness_ps = &eeprom_buffer.brightness_values;
static Color_st *const colors_ps = &eeprom_buffer.color_values;
static NightModeTimes_st *const night_mode_times_ps = &eeprom_buffer.night_mode_times;
// Games
static Pong pong = Pong(&led_matrix, &logger);
static Snake snake = Snake(&led_matrix, &logger);
static Tetris tetris = Tetris(&led_matrix, &logger);
// Time ManagerW
static TimeManager tm_mgr = TimeManager(MY_TZ, NTP_SERVER_URL,
NTP_UPDATE_PERIOD_S,
NTP_RETRY_DELAY_US,
NTP_MAX_OFFLINE_TIME_S,
&logger);
// State variablesW
static bool flg_night_mode = false; // State of nightmode
static bool flg_reset_wifi_creds = false; // Used to reset stored wifi credentials
static float filter_factor = DEFAULT_SMOOTHING_FACTOR; // Stores smoothing factor for led transition, value of 1 represents no smoothing.
static uint32_t main_color_clock = colors_24bit[2]; // Color of the clock and digital clock
static uint8_t current_brightness = DEFAULT_BRIGHTNESS; // Current brightness of LEDs
static uint8_t current_state = (uint8_t)ST_CLOCK; // Stores current state
// Other variables
static uint32 last_led_direct_us = 0; // Time of last direct LED command (=> fall back to normal mode after timeout)
static uint32_t heartbeat_counter = 0; // Heartbeat on-time in seconds
// Const definitions
static const String state_names[NUM_STATES] = {"Clock", "DiClock", "Spiral", "Tetris", "Snake", "PingPong", "Hearts"}; // all clock states
static const float qtly_brightness_factor[96] = { // Quarterly brightness factor for dynamic brightness (4 quarters a 24 hours)
0.0f, 0.0f, 0.0f, 0.001f, 0.003f, 0.007f, 0.014f, 0.026f, 0.044f, 0.069f, 0.101f, 0.143f, 0.194f, 0.253f, 0.32f,
0.392f, 0.468f, 0.545f, 0.62f, 0.691f, 0.755f, 0.811f, 0.858f, 0.896f, 0.927f, 0.949f, 0.966f, 0.978f, 0.986f,
0.991f, 0.995f, 0.997f, 0.998f, 0.999f, 0.999f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.999f, 0.999f,
0.998f, 0.997f, 0.995f, 0.991f, 0.986f, 0.978f, 0.966f, 0.949f, 0.927f, 0.896f, 0.858f, 0.811f, 0.755f, 0.691f,
0.62f, 0.545f, 0.468f, 0.392f, 0.32f, 0.253f, 0.194f, 0.143f, 0.101f, 0.069f, 0.044f, 0.026f, 0.014f, 0.007f,
0.003f, 0.001f, 0.0f, 0.0f};
static const uint32_t period_timings[NUM_STATES] = {PERIOD_TIME_UPDATE_US, PERIOD_TIME_UPDATE_US,
PERIOD_ANIMATION_US, PERIOD_TETRIS_US, PERIOD_SNAKE_US,
PERIOD_PONG_US, PERIOD_ANIMATION_US};
// ----------------------------------------------------------------------------------
// STATIC VARIABLES
// ----------------------------------------------------------------------------------
ESP8266Timer ITimer; // ESP8266 Timer
// ----------------------------------------------------------------------------------
// ISR
// ----------------------------------------------------------------------------------
void IRAM_ATTR TimerHandler()
{
tm_mgr.increment_time_now_local();
}
// ----------------------------------------------------------------------------------
// SETUP
// ----------------------------------------------------------------------------------
void setup()
{
// put your setup code here, to run once:
Serial.begin(115200);
delay(100);
Serial.println();
Serial.printf("\nSketchname: %s\nBuild: %s\n", (__FILE__), (__TIMESTAMP__));
Serial.println();
// Reset info
rst_info *reset_info = ESP.getResetInfoPtr();
Serial.printf("Reset reason: %u\n", reset_info->reason);
Serial.printf("Reset cause: %u\n", reset_info->exccause);
Serial.printf("Reset address: %u\n", reset_info->excvaddr);
Serial.println();
// Init EEPROM
EEPROM.begin(EEPROM_SIZE);
// Read global settings from EEPROM
read_settings_from_EEPROM();
// draw color on clock
draw_main_color();
// configure button pin as input
pinMode(BUTTON_PIN, INPUT_PULLUP);
// setup Matrix LED functions
led_matrix.setup_matrix();
led_matrix.set_current_limit(CURRENT_LIMIT_LED);
// Turn on minutes LEDs (blue)
led_matrix.set_min_indicator(15, colors_24bit[6]);
led_matrix.draw_on_matrix_instant();
/* Use WiFiMaanger for handling initial Wifi setup */
WiFiManager wifi_manager; // Local initialization. Once its business is done, there is no need to keep it around
/* fetches ssid and pass from eeprom and tries to connect. if it does not connect it starts an access point with
* the specified name and goes into a blocking loop awaiting configuration. */
wifi_manager.autoConnect(AP_SSID);
// If you get here you have connected to the WiFi
Serial.printf("Connected, IP address: ");
Serial.println(WiFi.localIP());
// ESP8266 tries to reconnect automatically when the connection is lost
WiFi.setAutoReconnect(true);
WiFi.persistent(true);
// Turn off minutes LEDs
led_matrix.set_min_indicator(15, 0);
led_matrix.draw_on_matrix_instant();
// init ESP8266 File manager (LittleFS)
setup_filesystem();
// set up OTA
setupOTA(HOSTNAME);
webserver.on("/cmd", handle_command); // process commands
webserver.on("/data", handle_data_request); // process data requests
webserver.on("/leddirect", HTTP_POST, handle_led_direct); // call the 'handle_led_direct' function when a POST request is made to URI "/leddirect"
webserver.begin();
// create UDP Logger to send logging messages via UDP multicast
logger = UDPLogger(WiFi.localIP(), LOGGER_MULTICAST_IP, LOGGER_MULTICAST_PORT, "Wordclock 2.0");
if (reset_info->reason != REASON_SOFT_RESTART) // only if there was a cold start/hard reset
{
cold_start_setup();
}
// get initial time
if (tm_mgr.ntp_time_update(true) == NTP_UPDATE_OK)
{
// show the current time for short time in words
String timeMessage = time_to_string(tm_mgr.tm_hour(), tm_mgr.tm_min());
show_string_on_clock(timeMessage, main_color_clock);
draw_minute_indicator(tm_mgr.tm_min(), main_color_clock);
led_matrix.draw_on_matrix_smooth(filter_factor);
}
else
{
logger.log_string("Warning: Initial time sync failed! Retrying in a bit.");
}
// init all animation modes
// init snake
random_snake(true, 8, colors_24bit[1], -1);
// init spiral
draw_spiral(true, spiral_direction, MATRIX_WIDTH - 6);
// init random tetris
random_tetris(true);
// Set range limits
limit_value_ranges();
// Update brightness
current_brightness = update_brightness();
// Send logging data
log_data();
}
// ----------------------------------------------------------------------------------
// LOOP
// ----------------------------------------------------------------------------------
void loop()
{
uint32 current_time_us = system_get_time();
// Timestamp variables
static uint32 last_animation_step_us = 0; // timestamp of last animation step
static uint32 last_matrix_update_us = 0; // timestamp of last Matrix update
static uint32 last_time_update_us = 0; // timestamp of last time update
static uint32 last_heartbeat_us = 0; // timestamp of last heartbeat sending
static uint32 last_nightmode_check_us = 0; // timestamp of last nightmode check
static uint32 last_brightness_update_us = 0; // timestamp of last brightness update
handleOTA(); // handle OTA
webserver.handleClient(); // handle webserver
handle_button(); // handle button press
// send regularly heartbeat messages via UDP multicast
if ((current_time_us - last_heartbeat_us) >= PERIOD_HEARTBEAT_US)
{
send_heartbeat(); // send heartbeat update
tm_mgr.log_time(); // TODO rm
last_heartbeat_us = system_get_time();
delay(10);
}
if (!flg_night_mode && ((current_time_us - last_animation_step_us) > period_timings[current_state]) &&
((current_time_us - last_led_direct_us) >= TIMEOUT_LEDDIRECT_US))
{
handle_current_state(); // handle current state
last_animation_step_us = system_get_time();
delay(10);
}
if ((current_time_us - last_brightness_update_us) >= PERIOD_BRIGHTNESS_UPDATE_US)
{
current_brightness = update_brightness(); // update brightness
logger.log_string("Brightness: " + String(((uint16_t)current_brightness * 100) / UINT8_MAX) + "%");
last_brightness_update_us = system_get_time();
delay(10);
}
if ((current_time_us - last_matrix_update_us) >= PERIOD_MATRIX_UPDATE_US)
{
update_matrix(); // update matrix
last_matrix_update_us = system_get_time();
delay(10);
}
if ((current_time_us - last_time_update_us) >= PERIOD_TIME_UPDATE_US)
{
if (tm_mgr.ntp_sync_successful() == true) // regular case
{
(void)tm_mgr.ntp_time_update(); // NTP time update
}
else // if there was never a NTP time update before (set up failed)
{
(void)tm_mgr.ntp_time_update(true); // NTP time update with init!
}
if (tm_mgr.ntp_update_failed_prolonged() == true)
{
logger.log_string("Trigger restart due to being offline for too long...");
delay(100);
ESP.restart();
}
}
if ((current_time_us - last_nightmode_check_us) >= PERIOD_NIGHTMODE_CHECK_US)
{
check_night_mode(); // check night mode
last_nightmode_check_us = system_get_time();
}
}
// ----------------------------------------------------------------------------------
// OTHER FUNCTIONS
// ----------------------------------------------------------------------------------
/**
* @brief Log information
*
*/
void log_data()
{
logger.log_string("Start program\n");
logger.log_string("Sketchname: " + String(__FILE__));
logger.log_string("Build: " + String(__TIMESTAMP__));
logger.log_string("IP: " + WiFi.localIP().toString());
logger.log_string("Reset Reason: " + ESP.getResetReason());
logger.log_string("Nightmode starts at: " + String(night_mode_times_ps->start_hour) + ":" + String(night_mode_times_ps->start_min));
logger.log_string("Nightmode ends at: " + String(night_mode_times_ps->end_hour) + ":" + String(night_mode_times_ps->end_min));
logger.log_string("Brightness: " + String(((uint16_t)current_brightness * 100) / UINT8_MAX) + "%\n");
}
/**
* @brief Test all LEDs and display IP address
*
*/
void cold_start_setup()
{
// quickly test each LED
for (int16_t row = 0; row < MATRIX_HEIGHT; row++)
{
for (int16_t col = 0; col < MATRIX_WIDTH; col++)
{
matrix.fillScreen(0);
matrix.drawPixel(col, row, LEDMatrix::color_24_to_16bit(colors_24bit[2]));
matrix.show();
delay(10);
}
}
// clear Matrix
matrix.fillScreen(0);
matrix.show();
delay(200);
// display IP
uint8_t address = WiFi.localIP()[3];
led_matrix.print_char(1, 0, 'I', main_color_clock);
led_matrix.print_char(5, 0, 'P', main_color_clock);
led_matrix.print_number(0, 6, (address / 100), main_color_clock);
led_matrix.print_number(4, 6, (address / 10) % 10, main_color_clock);
led_matrix.print_number(8, 6, address % 10, main_color_clock);
led_matrix.draw_on_matrix_instant();
delay(2000);
// clear matrix
led_matrix.flush();
led_matrix.draw_on_matrix_instant();
}
/**
* @brief Update and control word clock states.
*/
void handle_current_state()
{
switch (current_state)
{
case ST_CLOCK: // state clock
{
(void)show_string_on_clock(time_to_string((uint8_t)tm_mgr.tm_hour(), (uint8_t)tm_mgr.tm_min()), main_color_clock);
draw_minute_indicator((uint8_t)tm_mgr.tm_min(), main_color_clock);
break;
}
case ST_DICLOCK: // state diclock
{
show_digital_clock((uint8_t)tm_mgr.tm_hour(), (uint8_t)tm_mgr.tm_min(), main_color_clock);
break;
}
case ST_SPIRAL: // state spiral
{
int res = draw_spiral(false, spiral_direction, MATRIX_WIDTH - 2);
if ((bool)res && spiral_direction == 0)
{
// change spiral direction to closing (draw empty LEDs)
spiral_direction = true;
// init spiral with new spiral direction
draw_spiral(true, spiral_direction, MATRIX_WIDTH - 1);
}
else if (res && spiral_direction == 1)
{
// reset spiral direction to normal drawing LEDs
spiral_direction = false;
// init spiral with new spiral direction
draw_spiral(true, spiral_direction, MATRIX_WIDTH - 1);
}
break;
}
case ST_TETRIS: // state tetris
{
tetris.loopCycle();
break;
}
case ST_SNAKE: // state snake
{
snake.loopCycle();
break;
}
case ST_PINGPONG: // state ping pong
{
pong.loopCycle();
break;
}
case ST_HEARTS:
{
draw_heart_animation();
break;
}
default:
{
break;
}
}
}
/**
* @brief Update matrix colors, should be called in loop().
*
* @param None
*/
void update_matrix()
{
// periodically write colors to matrix
led_matrix.draw_on_matrix_smooth(filter_factor);
}
/**
* @brief Send heartbeat, should be called in loop().
*
* @param None
*/
void send_heartbeat()
{
logger.log_string("Current state: " + state_names[current_state] + ", counter: " +
heartbeat_counter + ", on-time: " + (float)(heartbeat_counter) / 3600.0 + "h\n");
heartbeat_counter++;
}
/**
* @brief Check WiFi status and try to reconnect if needed. Should be called in loop() before NTP update.
*
* @param None
*
* @retval bool - true if WiFi is connected, false otherwise
*/
bool check_wifi_status()
{
bool connected = (WiFi.status() == WL_CONNECTED);
// Check wifi status
if (!connected)
{
Serial.println("WiFi connection lost! Trying to reconnect automatically...");
}
return connected;
}
/**
* @brief Night mode check, should be called in loop().
*
* @param None
*/
void check_night_mode()
{
// check if nightmode need to be activated
int hours = tm_mgr.tm_hour();
int minutes = tm_mgr.tm_min();
if ((hours == night_mode_times_ps->start_hour) && (minutes == night_mode_times_ps->start_min))
{
set_night_mode(true);
}
else if ((hours == night_mode_times_ps->end_hour) && (minutes == night_mode_times_ps->end_min))
{
set_night_mode(false);
}
}
/**
* @brief call entry action of given state
*
* @param state
*/
void on_state_entry(uint8_t state)
{
filter_factor = DEFAULT_SMOOTHING_FACTOR;
switch (state)
{
case ST_SPIRAL:
{
spiral_direction = 0; // Init spiral with normal drawing mode
draw_spiral(true, spiral_direction, MATRIX_WIDTH - 1);
break;
}
case ST_TETRIS:
{
filter_factor = 1.0f; // no smoothing
tetris.ctrlStart();
break;
}
case ST_SNAKE:
{
filter_factor = 1.0f; // no smoothing
snake.initGame();
break;
}
case ST_PINGPONG:
{
filter_factor = 1.0f; // no smoothing
pong.initGame(1);
break;
}
default:
{
break;
}
}
}
/**
* @brief execute a state change to given newState
*
* @param newState the new state to be changed to
*/
void state_change(uint8_t newState)
{
if (flg_night_mode)
{
set_night_mode(false); // deactivate Nightmode
}
led_matrix.flush(); // first clear matrix
current_state = newState; // set new state
on_state_entry(current_state);
logger.log_string("State change to: " + state_names[current_state]);
logger.log_string("FreeMemory=" + String(ESP.getFreeHeap()));
}
/**
* @brief Handler for POST requests to /leddirect.
*
* Allows the control of all LEDs from external source.
* It will overwrite the normal program for 5 seconds.
* A 11x11 picture can be sent as base64 encoded string to be displayed on matrix.
*
*/
void handle_led_direct()
{
if (webserver.method() != HTTP_POST)
{
webserver.send(405, "text/plain", "Method Not Allowed");
}
else
{
String message = "POST data was:\n";
if (webserver.args() == 1)
{
String data = String(webserver.arg(0));
unsigned int dataLength = data.length();
// base64 decoding
char base64data[dataLength];
data.toCharArray(base64data, dataLength);
unsigned int decodedLength = decode_base64_length((unsigned char *)base64data, dataLength);
unsigned char byteArray[decodedLength];
decode_base64((unsigned char *)base64data, dataLength, byteArray);
for (unsigned int i = 0; i < dataLength; i += 4)
{
uint8_t red = byteArray[i]; // red
uint8_t green = byteArray[i + 1]; // green
uint8_t blue = byteArray[i + 2]; // blue
led_matrix.grid_add_pixel((i / 4) % MATRIX_WIDTH, (i / 4) / MATRIX_HEIGHT, LEDMatrix::color_24bit(red, green, blue));
}
led_matrix.draw_on_matrix_instant();
last_led_direct_us = system_get_time();
}
webserver.send(200, "text/plain", message);
}
}
/**
* @brief Check button commands
*
*/
void handle_button()
{
bool button_pressed = !digitalRead(BUTTON_PIN);
static bool last_button_state = false;
static uint32 button_press_start = 0; // time of push button press start
// check rising edge
if (button_pressed == true && last_button_state == false)
{
// button press start
logger.log_string("Button press started");
button_press_start = system_get_time();
}
// check falling edge
if (button_pressed == false && last_button_state == true)
{
// button press ended
if ((system_get_time() - button_press_start) > VERY_LONG_PRESS_US)
{
// longpress -> reset wifi creds and restart ESP
logger.log_string("Button press ended - very long press -> Reset Wifi creds.");
reset_wifi_credentials();
}
else if ((system_get_time() - button_press_start) > LONG_PRESS_US)
{
// longpress -> nightmode
logger.log_string("Button press ended - long press");
set_night_mode(true);
}
else if ((system_get_time() - button_press_start) > SHORT_PRESS_US)
{
// shortpress -> state change
logger.log_string("Button press ended - short press");
if (flg_night_mode)
{
set_night_mode(false);
}
else
{
state_change((current_state + 1) % (uint8_t)NUM_STATES);
}
}
}
last_button_state = button_pressed;
}
/**
* @brief Set main color
*
*/
void set_main_color(uint8_t red, uint8_t green, uint8_t blue)
{
main_color_clock = LEDMatrix::color_24bit(red, green, blue);
// Update colors and save color settings to EEPROM
colors_ps->blue = blue;
colors_ps->red = red;
colors_ps->green = green;
write_settings_to_EEPROM();
}
/**
* @brief Load main_color from EEPROM
*
*/
void draw_main_color()
{
uint8_t red = colors_ps->red;
uint8_t green = colors_ps->green;
uint8_t blue = colors_ps->blue;
if ((int(red) + int(green) + int(blue)) < 50)
{
main_color_clock = colors_24bit[2];
}
else
{
main_color_clock = LEDMatrix::color_24bit(red, green, blue);
}
}
/**
* @brief Handler for handling commands sent to "/cmd" url
*
*/
void handle_command()
{
// receive command and handle accordingly
for (uint8_t i = 0; i < webserver.args(); i++)
{
Serial.print(webserver.argName(i));
Serial.print(F(": "));
Serial.println(webserver.arg(i));
}
if (webserver.argName(0).equals("led")) // the parameter which was sent to this server is led color
{
String color_str = webserver.arg(0) + "-";
String red_str = split(color_str, '-', 0);
String green_str = split(color_str, '-', 1);
String blue_str = split(color_str, '-', 2);
logger.log_string(color_str);
logger.log_string("r: " + String(red_str.toInt()));
logger.log_string("g: " + String(green_str.toInt()));
logger.log_string("b: " + String(blue_str.toInt()));
// set new main color
set_main_color(red_str.toInt(), green_str.toInt(), blue_str.toInt());
}
else if (webserver.argName(0).equals("mode")) // the parameter which was sent to this server is mode change
{
String mode_str = webserver.arg(0);
logger.log_string("Mode change via Webserver to: " + mode_str);
// set current mode/state accordant sent mode
if (mode_str.equals("clock"))
{
state_change(ST_CLOCK);
}
else if (mode_str.equals("diclock"))
{
state_change(ST_DICLOCK);
}
else if (mode_str.equals("spiral"))
{
state_change(ST_SPIRAL);
}
else if (mode_str.equals("tetris"))
{
state_change(ST_TETRIS);
}
else if (mode_str.equals("snake"))
{
state_change(ST_SNAKE);
}
else if (mode_str.equals("pingpong"))
{
state_change(ST_PINGPONG);
}
else if (mode_str.equals("hearts"))
{
state_change(ST_HEARTS);
}
}
else if (webserver.argName(0).equals("night_mode"))
{
String mode_str = webserver.arg(0);
logger.log_string("Nightmode change via Webserver to: " + mode_str);
mode_str.equals("1") ? set_night_mode(true) : set_night_mode(false);
}
else if (webserver.argName(0).equals("dyn_brightness"))
{
String mode_str = webserver.arg(0);
logger.log_string("Dynamic brightness change via Webserver to: " + mode_str);
mode_str.equals("1") ? set_dynamic_brightness(true) : set_dynamic_brightness(false);
// Update brightness
current_brightness = update_brightness();
logger.log_string("Brightness: " + String(((uint16_t)current_brightness * 100) / UINT8_MAX) + "%");
}
else if (webserver.argName(0).equals("setting"))
{
String cmd_str = webserver.arg(0) + "-";
logger.log_string("Nightmode setting change via Webserver to: " + cmd_str);
night_mode_times_ps->start_hour = (int)split(cmd_str, '-', 0).toInt();
night_mode_times_ps->start_min = (int)split(cmd_str, '-', 1).toInt();
night_mode_times_ps->end_hour = (int)split(cmd_str, '-', 2).toInt();
night_mode_times_ps->end_min = (int)split(cmd_str, '-', 3).toInt();
brightness_ps->static_brightness = (uint8_t)split(cmd_str, '-', 4).toInt();
flg_reset_wifi_creds = split(cmd_str, '-', 5).toInt() > 0 ? true : false;
brightness_ps->dyn_brightness_min = (uint8_t)split(cmd_str, '-', 6).toInt();
brightness_ps->dyn_brightness_max = (uint8_t)split(cmd_str, '-', 7).toInt();
if (flg_reset_wifi_creds == true)
{
reset_wifi_credentials(); // this function will not return
}
limit_value_ranges();
// Update EEPROM with new settings
write_settings_to_EEPROM();
logger.log_string("Nightmode starts at: " + String(night_mode_times_ps->start_hour) + ":" + String(night_mode_times_ps->start_min));
delay(10);
logger.log_string("Nightmode ends at: " + String(night_mode_times_ps->end_hour) + ":" + String(night_mode_times_ps->end_min));
delay(10);
// Update brightness
current_brightness = update_brightness();
logger.log_string("Brightness: " + String(((uint16_t)current_brightness * 100) / UINT8_MAX) + "%");
}
else if (webserver.argName(0).equals("tetris"))
{
String cmd_str = webserver.arg(0);
// logger.log_string("Tetris cmd via Webserver to: " + cmd_str);
if (cmd_str.equals("up"))
{
tetris.ctrlUp();
}
else if (cmd_str.equals("left"))
{
tetris.ctrlLeft();
}
else if (cmd_str.equals("right"))
{
tetris.ctrlRight();
}
else if (cmd_str.equals("down"))
{
tetris.ctrlDown();
}
else if (cmd_str.equals("play"))
{
tetris.ctrlStart();
}
else if (cmd_str.equals("pause"))
{
tetris.ctrlPlayPause();
}
}
else if (webserver.argName(0).equals("snake"))
{
String cmd_str = webserver.arg(0);
// logger.log_string("Snake cmd via Webserver to: " + cmd_str);
if (cmd_str.equals("up"))
{
snake.ctrlUp();
}
else if (cmd_str.equals("left"))
{
snake.ctrlLeft();
}
else if (cmd_str.equals("right"))
{
snake.ctrlRight();
}
else if (cmd_str.equals("down"))
{
snake.ctrlDown();
}
else if (cmd_str.equals("new"))
{
snake.initGame();
}
}
else if (webserver.argName(0).equals("pong"))
{
String cmd_str = webserver.arg(0);
// logger.log_string("Pong cmd via Webserver to: " + cmd_str);
if (cmd_str.equals("up"))
{
pong.ctrlUp(1);
}
else if (cmd_str.equals("down"))
{
pong.ctrlDown(1);
}
else if (cmd_str.equals("new"))
{
pong.initGame(1);
}
}
webserver.send(204, "text/plain", "No Content"); // this page doesn't send back content --> 204
}
/**
* @brief Handler for GET requests
*
*/
void handle_data_request()
{
// receive data request and handle accordingly
for (uint8_t i = 0; i < webserver.args(); i++)
{
Serial.print(webserver.argName(i));
Serial.print(F(": "));
Serial.println(webserver.arg(i));
}
if (webserver.argName(0).equals("key"))
{
String message = "{";
String keystr = webserver.arg(0);
if (keystr.equals("mode"))
{
message += "\"mode\":\"" + state_names[current_state] + "\"";
message += ",";
message += "\"modeid\":\"" + String(current_state) + "\"";
message += ",";
message += "\"night_mode\":\"" + String(flg_night_mode) + "\"";
message += ",";
message += "\"nightModeStart\":\"" + leading_zero2digit(night_mode_times_ps->start_hour) + "-" + leading_zero2digit(night_mode_times_ps->start_min) + "\"";
message += ",";
message += "\"nightModeEnd\":\"" + leading_zero2digit(night_mode_times_ps->end_hour) + "-" + leading_zero2digit(night_mode_times_ps->end_min) + "\"";
message += ",";
message += "\"static_brightness\":\"" + String(brightness_ps->static_brightness) + "\"";
message += ",";
message += "\"dyn_brightness\":\"" + String(brightness_ps->flg_dynamic_brightness) + "\"";
message += ",";
message += "\"min_brightness\":\"" + String(brightness_ps->dyn_brightness_min) + "\"";
message += ",";
message += "\"max_brightness\":\"" + String(brightness_ps->dyn_brightness_max) + "\"";
}
message += "}";
webserver.send(200, "application/json", message);
}
}
/**
* @brief Set the nightmode state
*
* @param state true -> nightmode on
*/
void set_night_mode(bool state)
{
led_matrix.flush();
led_matrix.draw_on_matrix_smooth(0.2);
flg_night_mode = state;
}
/**
* @brief Set the dynamic brightness state
*
* @param state true -> nightmode on
*/
void set_dynamic_brightness(bool state)
{
brightness_ps->flg_dynamic_brightness = state;
}
/**
* @brief Convert Integer to String with leading zero
*
* @param value
* @return String
*/
String leading_zero2digit(int value)
{
String msg = "";
if (value < 10)
{
msg = "0";
}
msg += String(value);
return msg;
}
/**
* @brief Reset Wifi credentials and restart ESP. This function will not return.
*
*/
void reset_wifi_credentials()
{
WiFiManager wifi_manager;
wifi_manager.resetSettings();
delay(200);
ESP.restart();
}
/**
* @brief Calculate dynamic brightness value based on daytime.
*
* @param min_brightness max brightness value set by the user
* @param max_brightness min brightness value set by the user
* @param hours current hour value
* @param minutes current minute value
* @param summertime indicates if summertime
* @return dynamic brightness
*/
uint8_t calculate_dynamic_brightness(uint8_t min_brightness, uint8_t max_brightness,
int hours, int minutes, bool summertime)
{
uint8_t calc_brightness = 0;
uint8_t factor_index = 0;
// Calculate index based on current time, respecting array length (and wrap)
factor_index = (uint8_t)(((hours + (int)summertime) * MINUTES_IN_HOUR + minutes) /
(MINUTES_IN_HOUR / ((sizeof(qtly_brightness_factor) / sizeof(float)) / HOURS_IN_DAY))) %
(sizeof(qtly_brightness_factor) / sizeof(float));
// function for calc_brightness: f(x) = min + (max - min)(1 - cos((t * 2 * Pi) / (96))) / 2
calc_brightness = min_brightness +
(uint8_t)(((float)(max_brightness - min_brightness)) * qtly_brightness_factor[factor_index]);
// ouput limits
calc_brightness = RANGE_LIMIT(calc_brightness, MIN_BRIGHTNESS, UINT8_MAX);
return calc_brightness;
}
/**
* @brief Init function. Reads (global) user settings from EEPROM. Call this after EEPROM init!
*
* @return void
*/
void read_settings_from_EEPROM()
{
EEPROM.get(0, eeprom_buffer);
}
/**
* @brief Writes (global) user settings to EEPROM
*
* @return void
*/
void write_settings_to_EEPROM()
{
// Copy EEPROM buffer
EEPROM.put(0, eeprom_buffer);
// Commit changes
EEPROM.commit();
}
/**
* @brief Updates brightness based on chosen mode.
*
* @return newly calculated and set brightness
*/
uint8_t update_brightness()
{
uint8_t new_brightness = 0;
if (brightness_ps->flg_dynamic_brightness == true)
{
new_brightness = calculate_dynamic_brightness(brightness_ps->dyn_brightness_min,
brightness_ps->dyn_brightness_max,
tm_mgr.tm_hour(),
tm_mgr.tm_min(),
tm_mgr.tm_isdst());
}
else // use static brightness
{
new_brightness = brightness_ps->static_brightness;
}
// now set new brightness
led_matrix.set_brightness(new_brightness);
return new_brightness;
}
/**
* @brief Limits values ranges of user settings
*
* @return void
*/
void limit_value_ranges()
{
// Range limits
brightness_ps->dyn_brightness_min = RANGE_LIMIT(brightness_ps->dyn_brightness_min,
MIN_BRIGHTNESS,
brightness_ps->dyn_brightness_max - 1); // minimum brightness
brightness_ps->dyn_brightness_max = RANGE_LIMIT(brightness_ps->dyn_brightness_max,
brightness_ps->dyn_brightness_min + 1,
MAX_BRIGHTNESS); // maximum brightness
brightness_ps->static_brightness = RANGE_LIMIT(brightness_ps->static_brightness,
MIN_BRIGHTNESS,
MAX_BRIGHTNESS); // static brightness
night_mode_times_ps->start_hour = RANGE_LIMIT_SUB(night_mode_times_ps->start_hour,
0,
HOUR_MAX,
NIGHTMODE_START_HR);
night_mode_times_ps->start_min = RANGE_LIMIT_SUB(night_mode_times_ps->start_min,
0,
MINUTE_MAX,
NIGHTMODE_START_MIN);
night_mode_times_ps->end_hour = RANGE_LIMIT_SUB(night_mode_times_ps->end_hour,
0,
HOUR_MAX,
NIGHTMODE_END_HR);
night_mode_times_ps->end_min = RANGE_LIMIT_SUB(night_mode_times_ps->end_min,
0,
MINUTE_MAX,
NIGHTMODE_END_MIN);
}