/** * 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 #include "wordclock_esp8266.h" #include // https://github.com/adafruit/Adafruit-GFX-Library #include // https://github.com/adafruit/Adafruit_NeoMatrix #include // NeoPixel library used to run the NeoPixel LEDs: https://github.com/adafruit/Adafruit_NeoPixel #include //from ESP8266 Arduino Core (automatically installed when ESP8266 was installed via Boardmanager) #include #include #include //https://github.com/tzapu/WiFiManager WiFi Configuration Magic #include // own libraries #include "animation_functions.h" #include "base64_wrapper.h" // copied from https://github.com/Xander-Electronics/Base64 #include "led_matrix.h" #include "littlefs_wrapper.h" #include "ntp_client_plus.h" #include "ota_functions.h" #include "pong.h" #include "render_functions.h" #include "snake.h" #include "tetris.h" #include "udp_logger.h" #include "wordclock_constants.h" // ---------------------------------------------------------------------------------- // GLOBAL VARIABLES // ---------------------------------------------------------------------------------- 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 UDPLogger logger; // Logger ESP8266WebServer webserver(HTTP_PORT); // Webserver LEDMatrix led_matrix = LEDMatrix(&matrix, DEFAULT_BRIGHTNESS, &logger); // NeoMatrix wrapper WiFiUDP wifi_udp; NTPClientPlus ntp_client = NTPClientPlus(wifi_udp, NTP_SERVER_URL, 1, true); // ---------------------------------------------------------------------------------- // STATIC VARIABLES // ---------------------------------------------------------------------------------- static EepromLayout_st eeprom_buffer = {{0, 0, 0, 0}, {0U, 0U, 0U, false}, {0U, 0U, 0U, 0U}}; static Brightness_st *brightness_ps = &eeprom_buffer.brightness_values; static Color_st *colors_ps = &eeprom_buffer.color_values; static NightModeTimes_st *night_mode_times_ps = &eeprom_buffer.night_mode_times; static Pong pong = Pong(&led_matrix, &logger); static Snake snake = Snake(&led_matrix, &logger); static Tetris tetris = Tetris(&led_matrix, &logger); static bool flg_night_mode = false; // state of nightmode static bool flg_reset_wifi_creds = false; // used to reset stored wifi credentials static bool spiral_direction = false; static float filter_factor = DEFAULT_SMOOTHING_FACTOR; // stores smoothing factor for led transition static int watchdog_counter = 30; // Watchdog counter to trigger restart if NTP update was not possible 30 times in a row (5min) 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; // for heartbeat on-time in seconds static uint32_t main_color_clock = colors_24bit[2]; // color of the clock and digital clock static uint32_t main_color_snake = colors_24bit[1]; // color of the random snake animation static uint8_t current_brightness = DEFAULT_BRIGHTNESS; // current brightness of LEDs static uint8_t current_state = (uint8_t)ST_CLOCK; // stores current state static const String state_names[NUM_STATES] = {"Clock", "DiClock", "Spiral", "Tetris", "Snake", "PingPong", "Hearts"}; static const uint32_t period_timings[NUM_STATES] = {PERIOD_CLOCK_UPDATE_US, PERIOD_CLOCK_UPDATE_US, PERIOD_ANIMATION_US, PERIOD_TETRIS_US, PERIOD_SNAKE_US, PERIOD_PONG_US, PERIOD_ANIMATION_US}; // Quarterly brightness factor for dynamic brightness static const float qtly_brightness_factor[96] = { 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}; // ---------------------------------------------------------------------------------- // 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 *resetInfo = ESP.getResetInfoPtr(); Serial.printf("Reset reason: %u\n", resetInfo->reason); Serial.printf("Reset cause: %u\n", resetInfo->exccause); Serial.printf("Reset address: %u\n", resetInfo->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(); // setup 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"); logger.log_string("Start program\n"); delay(10); logger.log_string("Sketchname: " + String(__FILE__)); delay(10); logger.log_string("Build: " + String(__TIMESTAMP__)); delay(10); logger.log_string("IP: " + WiFi.localIP().toString()); delay(10); logger.log_string("Reset Reason: " + ESP.getResetReason()); if (resetInfo->reason != REASON_SOFT_RESTART) { // 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(); } // setup NTP ntp_client.setup_ntp_client(); logger.log_string("NTP running"); logger.log_string("Time: " + ntp_client.get_formatted_time()); logger.log_string("TimeOffset (seconds): " + String(ntp_client.get_time_offset())); // show the current time for short time in words int hours = ntp_client.get_hours_24(); int minutes = ntp_client.get_minutes(); String timeMessage = time_to_string(hours, minutes); show_string_on_clock(timeMessage, main_color_clock); draw_minute_indicator(minutes, main_color_clock); led_matrix.draw_on_matrix_smooth(filter_factor); // 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); // Range limits limit_value_ranges(); 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)); // Update brightness current_brightness = update_brightness(); logger.log_string("Brightness: " + String(((uint16_t)current_brightness * 100) / UINT8_MAX) + "%"); } // ---------------------------------------------------------------------------------- // LOOP // ---------------------------------------------------------------------------------- void loop() { uint32 current_time_us = system_get_time(); // Timestamp variables static uint32 last_animation_step_us = 0; // time of last animation step static uint32 last_matrix_update_us = 0; // time of last Matrix update static uint32 last_heartbeat_us = 0; // time of last heartbeat sending static uint32 last_nightmode_check_us = 0; // time of last nightmode check static uint32 last_ntp_update_us = 0; // time of last NTP update handleOTA(); // handle OTA webserver.handleClient(); // handle webserver // send regularly heartbeat messages via UDP multicast if ((current_time_us - last_heartbeat_us) > PERIOD_HEARTBEAT_US) { send_heartbeat(); // send heartbeat update 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_matrix_update_us) > PERIOD_MATRIX_UPDATE_US) { update_matrix(); // update matrix last_matrix_update_us = system_get_time(); delay(10); } handle_button(); // handle button press if ((current_time_us - last_ntp_update_us) > PERIOD_NTP_UPDATE_US) { check_wifi_status(); // check WiFi status before NTP update delay(10); ntp_time_update(&last_ntp_update_us); // NTP time update delay(10); current_brightness = update_brightness(); // update brightness every PERIOD_NTP_UPDATE_US delay(10); logger.log_string("Brightness: " + String(((uint16_t)current_brightness * 100) / UINT8_MAX) + "%"); delay(10); } 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 // ---------------------------------------------------------------------------------- void handle_current_state() { switch (current_state) { case ST_CLOCK: // state clock { int hours = ntp_client.get_hours_24(); int minutes = ntp_client.get_minutes(); (void)show_string_on_clock(time_to_string((uint8_t)hours, (uint8_t)minutes), main_color_clock); draw_minute_indicator((uint8_t)minutes, main_color_clock); break; } case ST_DICLOCK: // state diclock { int hours = ntp_client.get_hours_24(); int minutes = ntp_client.get_minutes(); show_digital_clock((uint8_t)hours, (uint8_t)minutes, 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 */ void check_wifi_status() { // Check wifi status if (WiFi.status() != WL_CONNECTED) { Serial.println("WiFi connection lost! Trying to reconnect automatically..."); } } /** * @brief Night mode check, should be called in loop(). * * @param None */ void check_night_mode() { // check if nightmode need to be activated int hours = ntp_client.get_hours_24(); int minutes = ntp_client.get_minutes(); 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 NTP time update, should be called in loop(). * * @param None */ void ntp_time_update(uint32 *last_ntp_update_us) { // NTP time update int ntp_retval = ntp_client.update_ntp(); switch (ntp_retval) { case NTP_UPDATE_SUCCESS: { ntp_client.calc_date(); logger.log_string("NTP-Update successful, Time: " + ntp_client.get_formatted_time()); *last_ntp_update_us = system_get_time(); watchdog_counter = 30; break; } case NTP_UPDATE_TIMEOUT: { logger.log_string("NTP-Update not successful. Reason: Timeout"); *last_ntp_update_us += 10000000; watchdog_counter--; break; } case NTP_UPDATE_DIFFTOOHIGH: { logger.log_string("NTP-Update not successful. Reason: Too large time difference"); logger.log_string("Time: " + ntp_client.get_formatted_time()); logger.log_string("Date: " + ntp_client.get_formatted_date()); logger.log_string("TimeOffset (seconds): " + String(ntp_client.get_time_offset())); logger.log_string("Summertime: " + String(ntp_client.check_daylight_saving_time())); *last_ntp_update_us += 10000000; watchdog_counter--; break; } case NTP_UPDATE_TIME_INVALID: default: { logger.log_string("NTP-Update not successful. Reason: NTP time not valid (<1970)"); *last_ntp_update_us += 10000000; watchdog_counter--; break; } } logger.log_string("Watchdog counter: " + String(watchdog_counter)); if (watchdog_counter <= 0) { logger.log_string("Trigger restart due to watchdog..."); delay(100); ESP.restart(); } } /** * @brief call entry action of given state * * @param state */ void on_state_entry(uint8_t state) { filter_factor = 0.5f; 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); int base64dataLen = (int)dataLength; int decodedLength = Base64.decodedLength(base64data, base64dataLen); char byteArray[decodedLength]; Base64.decode(byteArray, base64data, base64dataLen); 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); colors_ps->blue = blue; colors_ps->red = red; colors_ps->green = green; // save color settings to EEPROM 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, ntp_client.get_hours_24(), ntp_client.get_minutes(), ntp_client.check_daylight_saving_time()); } 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); }