/** * 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 #include // from ESP8266 Arduino Core (automatically installed when ESP8266 was installed via Boardmanager) #include // https://github.com/khoih-prog/ESP8266TimerInterrupt #include // https://github.com/khoih-prog/ESP8266TimerInterrupt #include #include #include // 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() == 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 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_time_update() == NTP_UPDATE_OK) // NTP time update { logger.log_string("NTP sync successful!"); } 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 { if (tm_mgr.ntp_sync_successful() && tm_mgr.tm_state() == TM_NORMAL) { (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); } else if (tm_mgr.ntp_sync_successful() && tm_mgr.tm_state() == TM_RETRY_SYNC) { (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(), colors_24bit[6]); // in blue to indicate a network problem } else { // clear matrix led_matrix.flush(); // Turn on minutes LEDs (blue) led_matrix.set_min_indicator(15, colors_24bit[6]); led_matrix.draw_on_matrix_instant(); } break; } case ST_DICLOCK: // state diclock { if (tm_mgr.ntp_sync_successful()) { show_digital_clock((uint8_t)tm_mgr.tm_hour(), (uint8_t)tm_mgr.tm_min(), main_color_clock); } else { // clear matrix led_matrix.flush(); // Turn on minutes LEDs (blue) led_matrix.set_min_indicator(15, colors_24bit[6]); led_matrix.draw_on_matrix_instant(); } 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); }