From b424321bc0e39ddfe9dcef7c878eca0cf71ac09a Mon Sep 17 00:00:00 2001 From: Markus Ransberger Date: Sat, 13 Dec 2025 03:36:06 +0100 Subject: [PATCH] Initial commit --- .gitignore | 79 + .vscode/settings.json | 66 + LICENSE | 21 + README.md | 42 + include/animation_functions.h | 24 + include/compile_time.h | 97 + include/diagnosis.h | 25 + include/led_matrix.h | 60 + include/littlefs_wrapper.h | 29 + include/ota_wrapper.h | 9 + include/own_font.h | 20 + include/pong.h | 100 + include/render_functions.h | 11 + include/snake.h | 94 + include/tetris.h | 199 ++ include/udp_logger.h | 37 + include/wordclock_constants.h | 79 + include/wordclock_esp32.h | 89 + lib/README | 46 + platformio.ini | 30 + res/frontplate/WordClock_DrillingTemplate.pdf | Bin 0 -> 32838 bytes res/frontplate/WordClock_DrillingTemplate.svg | 1828 ++++++++++++++++ .../WordClock_DrillingTemplate_Overlay.svg | 1828 ++++++++++++++++ res/frontplate/WordClock_Front.svg | 1301 +++++++++++ res/frontplate/WordClock_Front_Paths.svg | 775 +++++++ .../WordClock_Front_Paths_Inkscape.svg | 808 +++++++ .../frontplate_wordclock2.0_english.svg | 1912 +++++++++++++++++ .../frontplate_wordclock2.0_german.svg | 1912 +++++++++++++++++ .../frontplate_wordclock2.0_italian.svg | 1912 +++++++++++++++++ res/webserver/fs.html | 77 + res/webserver/icons/all_icons.svg | 273 +++ res/webserver/icons/arrow_left.svg | 15 + res/webserver/icons/arrow_right.svg | 15 + res/webserver/icons/clock.svg | 25 + res/webserver/icons/diclock.svg | 20 + res/webserver/icons/hearts.svg | 71 + res/webserver/icons/pause.svg | 16 + res/webserver/icons/pingpong.svg | 23 + res/webserver/icons/play.svg | 15 + res/webserver/icons/playpause.svg | 20 + res/webserver/icons/refresh.svg | 18 + res/webserver/icons/settings.svg | 29 + res/webserver/icons/snake.svg | 22 + res/webserver/icons/spiral.svg | 19 + res/webserver/icons/tetris.svg | 24 + res/webserver/index.html | 702 ++++++ res/webserver/style32.css | 108 + scripts/http_diagnosis.py | 13 + scripts/multicastUDP_receiver.py | 52 + scripts/requirements.txt | Bin 0 -> 184 bytes src/connectivity/diagnosis.cpp | 87 + src/connectivity/ota_wrapper.cpp | 54 + src/connectivity/udp_logger.cpp | 44 + src/games/pong.cpp | 332 +++ src/games/snake.cpp | 315 +++ src/games/tetris.cpp | 680 ++++++ src/matrix/animation_functions.cpp | 1192 ++++++++++ src/matrix/led_matrix.cpp | 344 +++ src/matrix/render_functions.cpp | 296 +++ src/wordclock_esp32.cpp | 1094 ++++++++++ src/wrapper/littlefs_wrapper.cpp | 230 ++ test/README | 11 + 62 files changed, 19669 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 include/animation_functions.h create mode 100644 include/compile_time.h create mode 100644 include/diagnosis.h create mode 100644 include/led_matrix.h create mode 100644 include/littlefs_wrapper.h create mode 100644 include/ota_wrapper.h create mode 100644 include/own_font.h create mode 100644 include/pong.h create mode 100644 include/render_functions.h create mode 100644 include/snake.h create mode 100644 include/tetris.h create mode 100644 include/udp_logger.h create mode 100644 include/wordclock_constants.h create mode 100644 include/wordclock_esp32.h create mode 100644 lib/README create mode 100644 platformio.ini create mode 100644 res/frontplate/WordClock_DrillingTemplate.pdf create mode 100644 res/frontplate/WordClock_DrillingTemplate.svg create mode 100644 res/frontplate/WordClock_DrillingTemplate_Overlay.svg create mode 100644 res/frontplate/WordClock_Front.svg create mode 100644 res/frontplate/WordClock_Front_Paths.svg create mode 100644 res/frontplate/WordClock_Front_Paths_Inkscape.svg create mode 100644 res/frontplate/original/frontplate_wordclock2.0_english.svg create mode 100644 res/frontplate/original/frontplate_wordclock2.0_german.svg create mode 100644 res/frontplate/original/frontplate_wordclock2.0_italian.svg create mode 100644 res/webserver/fs.html create mode 100644 res/webserver/icons/all_icons.svg create mode 100644 res/webserver/icons/arrow_left.svg create mode 100644 res/webserver/icons/arrow_right.svg create mode 100644 res/webserver/icons/clock.svg create mode 100644 res/webserver/icons/diclock.svg create mode 100644 res/webserver/icons/hearts.svg create mode 100644 res/webserver/icons/pause.svg create mode 100644 res/webserver/icons/pingpong.svg create mode 100644 res/webserver/icons/play.svg create mode 100644 res/webserver/icons/playpause.svg create mode 100644 res/webserver/icons/refresh.svg create mode 100644 res/webserver/icons/settings.svg create mode 100644 res/webserver/icons/snake.svg create mode 100644 res/webserver/icons/spiral.svg create mode 100644 res/webserver/icons/tetris.svg create mode 100644 res/webserver/index.html create mode 100644 res/webserver/style32.css create mode 100644 scripts/http_diagnosis.py create mode 100644 scripts/multicastUDP_receiver.py create mode 100644 scripts/requirements.txt create mode 100644 src/connectivity/diagnosis.cpp create mode 100644 src/connectivity/ota_wrapper.cpp create mode 100644 src/connectivity/udp_logger.cpp create mode 100644 src/games/pong.cpp create mode 100644 src/games/snake.cpp create mode 100644 src/games/tetris.cpp create mode 100644 src/matrix/animation_functions.cpp create mode 100644 src/matrix/led_matrix.cpp create mode 100644 src/matrix/render_functions.cpp create mode 100644 src/wordclock_esp32.cpp create mode 100644 src/wrapper/littlefs_wrapper.cpp create mode 100644 test/README diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a93f302 --- /dev/null +++ b/.gitignore @@ -0,0 +1,79 @@ +# PlatformIO +.pio + +# VS Code +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/extensions.json +.vscode/ipch +.vscode/launch.json + +# Own folders +_unused/ + +# Own files +log.txt + +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +# Python +venv +.venv + +# Logs +*.log + +# Local backup +*.*_ +*.*bak* diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7460d56 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,66 @@ +{ + "cmake.configureOnOpen": false, + "editor.rulers": [ + 120 + ], + "files.associations": { + "array": "cpp", + "atomic": "cpp", + "*.tcc": "cpp", + "cctype": "cpp", + "chrono": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "deque": "cpp", + "list": "cpp", + "unordered_map": "cpp", + "unordered_set": "cpp", + "vector": "cpp", + "exception": "cpp", + "algorithm": "cpp", + "functional": "cpp", + "iterator": "cpp", + "map": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "optional": "cpp", + "random": "cpp", + "ratio": "cpp", + "string": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "fstream": "cpp", + "initializer_list": "cpp", + "iomanip": "cpp", + "iosfwd": "cpp", + "iostream": "cpp", + "istream": "cpp", + "limits": "cpp", + "new": "cpp", + "ostream": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "thread": "cpp", + "cinttypes": "cpp", + "typeinfo": "cpp", + "condition_variable": "cpp", + "csignal": "cpp", + "set": "cpp", + "mutex": "cpp", + "*.h_": "cpp" + } +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6683081 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Techniccontroller, Ranse + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b56dc8c --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# Important Note: +This project has been unofficially forked from https://github.com/techniccontroller/wordclock_esp8266 which was initially created by techniccontroller. Copyright and licensing is respected. Very many thanks for the initial code! + + +# Wordclock 2.0 + +Wordclock 2.0 with ESP32 and NTP time. + + +**Languages** + +The Wordclock is available in **German** language. + + +## Features +- Time update via NTP server +- Games: Pong, Snake, Tetris, ... +- Automatic summer/wintertime change +- Easy wifi setup with WifiManager +- Configurable color +- Configurable night mode (start and end time) +- Configurable brightness +- Automatic mode change +- Webserver interface for configuration and control (web address: wordclock.local) +- Automatic current limiting of LEDs + +## Pictures of clock +![modes_images2](https://user-images.githubusercontent.com/36072504/156947689-dd90874d-a887-4254-bede-4947152d85c1.png) + +## Quickstart + +1. Clone the project into the a directory. +2. Open directory in VSCode with PlatformIO extension installed. +3. Current dependencies (TBD): +- Platform TBD +- Libraries TBD + + + +## Remark about the WiFi setup + +By default the WifiManager is activated. That is, the word clock makes the first time its own WiFi (should be called "WordclockAP"). There you simply connect to the cell phone and you can perform configuration of the WiFi settings conveniently as with SmartHome devices. diff --git a/include/animation_functions.h b/include/animation_functions.h new file mode 100644 index 0000000..62f4d38 --- /dev/null +++ b/include/animation_functions.h @@ -0,0 +1,24 @@ +#ifndef ANIMATIONFUNCTIONS_H +#define ANIMATIONFUNCTIONS_H + +#include +#include "wordclock_constants.h" + +extern bool spiral_direction; // Direction of sprial animation + +enum Direction +{ + RIGHT, + LEFT, + UP, + DOWN +}; + +Direction next_direction(Direction dir, int d); +int random_snake(bool init, const uint8_t len, const uint32_t color, int numSteps); +int random_tetris(bool init); +int draw_heart_animation(void); +int draw_spiral(bool init, bool empty, uint8_t size); +void show_digital_clock(uint8_t hours, uint8_t minutes, uint32_t color); + +#endif /* ANIMATIONFUNCTIONS_H */ diff --git a/include/compile_time.h b/include/compile_time.h new file mode 100644 index 0000000..ce34206 --- /dev/null +++ b/include/compile_time.h @@ -0,0 +1,97 @@ +/* + * + * Created: 29.03.2018 + * + * Authors: + * + * Assembled from the code released on Stackoverflow by: + * Dennis (instructable.com/member/nqtronix) | https://stackoverflow.com/questions/23032002/c-c-how-to-get-integer-unix-timestamp-of-build-time-not-string + * and + * Alexis Wilke | https://stackoverflow.com/questions/10538444/do-you-know-of-a-c-macro-to-compute-unix-time-and-date + * + * Assembled by Jean Rabault + * + * UNIX_TIMESTAMP gives the UNIX timestamp (unsigned long integer of seconds since 1st Jan 1970) of compilation from macros using the compiler defined __TIME__ macro. + * This should include Gregorian calendar leap days, in particular the 29ths of February, 100 and 400 years modulo leaps. + * + * Careful: __TIME__ is the local time of the computer, NOT the UTC time in general! + * + */ + +#ifndef COMPILE_TIME_H_ +#define COMPILE_TIME_H_ + +// Some definitions for calculation +#define SEC_PER_MIN 60UL +#define SEC_PER_HOUR 3600UL +#define SEC_PER_DAY 86400UL +#define SEC_PER_YEAR (SEC_PER_DAY*365) + +// extracts 1..4 characters from a string and interprets it as a decimal value +#define CONV_STR2DEC_1(str, i) (str[i]>'0'?str[i]-'0':0) +#define CONV_STR2DEC_2(str, i) (CONV_STR2DEC_1(str, i)*10 + str[i+1]-'0') +#define CONV_STR2DEC_3(str, i) (CONV_STR2DEC_2(str, i)*10 + str[i+2]-'0') +#define CONV_STR2DEC_4(str, i) (CONV_STR2DEC_3(str, i)*10 + str[i+3]-'0') + +// Custom "glue logic" to convert the month name to a usable number +#define GET_MONTH(str, i) (str[i]=='J' && str[i+1]=='a' && str[i+2]=='n' ? 1 : \ + str[i]=='F' && str[i+1]=='e' && str[i+2]=='b' ? 2 : \ + str[i]=='M' && str[i+1]=='a' && str[i+2]=='r' ? 3 : \ + str[i]=='A' && str[i+1]=='p' && str[i+2]=='r' ? 4 : \ + str[i]=='M' && str[i+1]=='a' && str[i+2]=='y' ? 5 : \ + str[i]=='J' && str[i+1]=='u' && str[i+2]=='n' ? 6 : \ + str[i]=='J' && str[i+1]=='u' && str[i+2]=='l' ? 7 : \ + str[i]=='A' && str[i+1]=='u' && str[i+2]=='g' ? 8 : \ + str[i]=='S' && str[i+1]=='e' && str[i+2]=='p' ? 9 : \ + str[i]=='O' && str[i+1]=='c' && str[i+2]=='t' ? 10 : \ + str[i]=='N' && str[i+1]=='o' && str[i+2]=='v' ? 11 : \ + str[i]=='D' && str[i+1]=='e' && str[i+2]=='c' ? 12 : 0) + +// extract the information from the time string given by __TIME__ and __DATE__ +#define __TIME_SECONDS__ CONV_STR2DEC_2(__TIME__, 6) +#define __TIME_MINUTES__ CONV_STR2DEC_2(__TIME__, 3) +#define __TIME_HOURS__ CONV_STR2DEC_2(__TIME__, 0) +#define __TIME_DAYS__ CONV_STR2DEC_2(__DATE__, 4) +#define __TIME_MONTH__ GET_MONTH(__DATE__, 0) +#define __TIME_YEARS__ CONV_STR2DEC_4(__DATE__, 7) + +// Days in February +#define _UNIX_TIMESTAMP_FDAY(year) \ + (((year) % 400) == 0UL ? 29UL : \ + (((year) % 100) == 0UL ? 28UL : \ + (((year) % 4) == 0UL ? 29UL : \ + 28UL))) + +// Days in the year +#define _UNIX_TIMESTAMP_YDAY(year, month, day) \ + ( \ + /* January */ day \ + /* February */ + (month >= 2 ? 31UL : 0UL) \ + /* March */ + (month >= 3 ? _UNIX_TIMESTAMP_FDAY(year) : 0UL) \ + /* April */ + (month >= 4 ? 31UL : 0UL) \ + /* May */ + (month >= 5 ? 30UL : 0UL) \ + /* June */ + (month >= 6 ? 31UL : 0UL) \ + /* July */ + (month >= 7 ? 30UL : 0UL) \ + /* August */ + (month >= 8 ? 31UL : 0UL) \ + /* September */+ (month >= 9 ? 31UL : 0UL) \ + /* October */ + (month >= 10 ? 30UL : 0UL) \ + /* November */ + (month >= 11 ? 31UL : 0UL) \ + /* December */ + (month >= 12 ? 30UL : 0UL) \ + ) + +// get the UNIX timestamp from a digits representation +#define _UNIX_TIMESTAMP(year, month, day, hour, minute, second) \ + ( /* time */ second \ + + minute * SEC_PER_MIN \ + + hour * SEC_PER_HOUR \ + + /* year day (month + day) */ (_UNIX_TIMESTAMP_YDAY(year, month, day) - 1) * SEC_PER_DAY \ + + /* year */ (year - 1970UL) * SEC_PER_YEAR \ + + ((year - 1969UL) / 4UL) * SEC_PER_DAY \ + - ((year - 1901UL) / 100UL) * SEC_PER_DAY \ + + ((year - 1601UL) / 400UL) * SEC_PER_DAY \ + ) + +// the UNIX timestamp +#define UNIX_TIMESTAMP (_UNIX_TIMESTAMP(__TIME_YEARS__, __TIME_MONTH__, __TIME_DAYS__, __TIME_HOURS__, __TIME_MINUTES__, __TIME_SECONDS__)) + +#endif diff --git a/include/diagnosis.h b/include/diagnosis.h new file mode 100644 index 0000000..fec7c31 --- /dev/null +++ b/include/diagnosis.h @@ -0,0 +1,25 @@ +#ifndef DIAGNOSIS_H +#define DIAGNOSIS_H + +#include +#include "led_matrix.h" +#include "udp_logger.h" + +class Diagnosis +{ +public: + Diagnosis(UDPLogger *logger, LEDMatrix *matrix); // constructor + + String handle_command(const String &command); + String print_device_info(); + String print_sketch_info(); + String print_last_reset_details(); + String print_matrix_fps(); + +private: + UDPLogger *_logger; + LEDMatrix * _matrix; + void print(const String &s); +}; + +#endif // DIAGNOSIS_H \ No newline at end of file diff --git a/include/led_matrix.h b/include/led_matrix.h new file mode 100644 index 0000000..ee0457b --- /dev/null +++ b/include/led_matrix.h @@ -0,0 +1,60 @@ +#ifndef LEDMATRIX_H +#define LEDMATRIX_H + +#ifndef FASTLED_INTERNAL +#define FASTLED_INTERNAL +#endif + +#include +#include +#include +#include "wordclock_constants.h" +#include "udp_logger.h" + +#define DEFAULT_CURRENT_LIMIT 9999 + +extern const uint32_t colors_24bit[NUM_COLORS]; +class LEDMatrix +{ +public: + LEDMatrix(FastLED_NeoMatrix *matrix, uint8_t brightness, UDPLogger *logger); + static uint16_t color_24_to_16bit(uint32_t color24bit); + static uint32_t color_24bit(uint8_t r, uint8_t g, uint8_t b); + static uint32_t interpolate_color_24bit(uint32_t color1, uint32_t color2, float factor); + static uint32_t wheel(uint8_t WheelPos); + uint16_t get_fps(void); + void draw_on_matrix_instant(); + void draw_on_matrix_smooth(float factor); + void flush(void); + void grid_add_pixel(uint8_t x, uint8_t y, uint32_t color); + void print_char(uint8_t xpos, uint8_t ypos, char character, uint32_t color); + void print_number(uint8_t xpos, uint8_t ypos, uint8_t number, uint32_t color); + void set_brightness(uint8_t mybrightness); + void set_current_limit(uint16_t new_current_limit); + void set_min_indicator(uint8_t pattern, uint32_t color); + void setup_matrix(); + +private: + FastLED_NeoMatrix *_neomatrix; + UDPLogger *_logger; + + uint8_t _brightness; + uint16_t _current_limit; + + // target representation of matrix as 2D array + uint32_t _target_grid[MATRIX_HEIGHT][MATRIX_WIDTH]; + + // current representation of matrix as 2D array + uint32_t _current_grid[MATRIX_HEIGHT][MATRIX_WIDTH]; + + // target representation of minutes indicator LEDs + uint32_t _target_minute_indicators[4] = {0, 0, 0, 0}; + + // current representation of minutes indicator LEDs + uint32_t _current_minute_indicators[4] = {0, 0, 0, 0}; + + void _draw_on_matrix(float factor); + uint16_t _calc_estimated_led_current(uint32_t color); +}; + +#endif /* LEDMATRIX_H */ \ No newline at end of file diff --git a/include/littlefs_wrapper.h b/include/littlefs_wrapper.h new file mode 100644 index 0000000..43625a8 --- /dev/null +++ b/include/littlefs_wrapper.h @@ -0,0 +1,29 @@ +#ifndef LITTLEFS_WRAPPER_H +#define LITTLEFS_WRAPPER_H + +#include + +#define USE_LittleFS + +//#define DEBUGGING // Einkommentieren für die Serielle Ausgabe + +#ifdef DEBUGGING +#define DEBUG_B(...) Serial.begin(__VA_ARGS__) +#define DEBUG_P(...) Serial.println(__VA_ARGS__) +#define DEBUG_F(...) Serial.printf(__VA_ARGS__) +#else +#define DEBUG_B(...) +#define DEBUG_P(...) +#define DEBUG_F(...) +#endif + +bool handle_file(String &&path); +bool handle_list(); +const String format_bytes(size_t const &bytes); +void delete_recursive(const String &path); +void format_filesystem(); +void handle_upload(); +void send_response(); +void setup_filesystem(); + +#endif /* LITTLEFS_WRAPPER_H */ diff --git a/include/ota_wrapper.h b/include/ota_wrapper.h new file mode 100644 index 0000000..3940b06 --- /dev/null +++ b/include/ota_wrapper.h @@ -0,0 +1,9 @@ +#ifndef OTA_FUNCTIONS_H +#define OTA_FUNCTIONS_H + +#include + +void handleOTA(); +void setupOTA(String hostname); + +#endif /* OTA_FUNCTIONS_H */ diff --git a/include/own_font.h b/include/own_font.h new file mode 100644 index 0000000..3d56572 --- /dev/null +++ b/include/own_font.h @@ -0,0 +1,20 @@ +#ifndef OWN_FONT_H +#define OWN_FONT_H + +#include + +uint8_t numbers_font[10][5] = {{0b00000111, 0b00000101, 0b00000101, 0b00000101, 0b00000111}, + {0b00000001, 0b00000001, 0b00000001, 0b00000001, 0b00000001}, + {0b00000111, 0b00000001, 0b00000111, 0b00000100, 0b00000111}, + {0b00000111, 0b00000001, 0b00000111, 0b00000001, 0b00000111}, + {0b00000101, 0b00000101, 0b00000111, 0b00000001, 0b00000001}, + {0b00000111, 0b00000100, 0b00000111, 0b00000001, 0b00000111}, + {0b00000111, 0b00000100, 0b00000111, 0b00000101, 0b00000111}, + {0b00000111, 0b00000001, 0b00000001, 0b00000001, 0b00000001}, + {0b00000111, 0b00000101, 0b00000111, 0b00000101, 0b00000111}, + {0b00000111, 0b00000101, 0b00000111, 0b00000001, 0b00000111}}; + +uint8_t chars_font[2][5] = {{0b00000010, 0b00000010, 0b00000010, 0b00000010, 0b00000010}, + {0b00000111, 0b00000101, 0b00000111, 0b00000100, 0b00000100}}; + +#endif /* OWN_FONT_H */ diff --git a/include/pong.h b/include/pong.h new file mode 100644 index 0000000..e27fc79 --- /dev/null +++ b/include/pong.h @@ -0,0 +1,100 @@ +/** + * @file pong.h + * @author techniccontroller (mail[at]techniccontroller.com) + * @brief Class declaration for pong game + * @version 0.1 + * @date 2022-03-05 + * + * @copyright Copyright (c) 2022 + * + * main code from https://elektro.turanis.de/html/prj041/index.html + * + */ + +#ifndef PONG_H +#define PONG_H + +#include +#include "led_matrix.h" +#include "udp_logger.h" + +#ifdef DEBOUNCE_TIME +#undef DEBOUNCE_TIME +#endif +#define DEBOUNCE_TIME 10 // in ms + +#define X_MAX 11 +#define Y_MAX 11 + +#ifdef GAME_DELAY +#undef GAME_DELAY +#endif +#define GAME_DELAY 80 // in ms + +#define BALL_DELAY_MAX 350 // in ms +#define BALL_DELAY_MIN 50 // in ms +#define BALL_DELAY_STEP 5 // in ms + +#define PLAYER_AMOUNT 2 +#define PLAYER_1 0 +#define PLAYER_2 1 + +#define PADDLE_WIDTH 3 + +#define PADDLE_MOVE_NONE 0 +#define PADDLE_MOVE_UP 1 +#define PADDLE_MOVE_DOWN 2 + +#ifdef LED_TYPE_OFF +#undef LED_TYPE_OFF +#endif +#define LED_TYPE_OFF 1 +#define LED_TYPE_PADDLE 2 +#define LED_TYPE_BALL 3 +#define LED_TYPE_BALL_RED 4 + +#define GAME_STATE_RUNNING 1 +#define GAME_STATE_END 2 +#define GAME_STATE_INIT 3 + +class Pong +{ + struct Coords + { + uint8_t x; + uint8_t y; + }; + +public: + Pong(); + Pong(LEDMatrix * matrix, UDPLogger * logger); + void loopCycle(); + void initGame(uint8_t numBots); + void ctrlUp(uint8_t playerid); + void ctrlDown(uint8_t playerid); + void ctrlNone(uint8_t playerid); + +private: + LEDMatrix *_ledmatrix; + UDPLogger *_logger; + uint8_t _gameState = 0; + uint8_t _numBots = 0; + uint8_t _playerMovement[PLAYER_AMOUNT]; + Coords _paddles[PLAYER_AMOUNT][PADDLE_WIDTH]; + Coords _ball = {0, 0}; + Coords _ball_old = {0, 0}; + int _ballMovement[2] = {0, 0}; + unsigned int _ballDelay = 0; + unsigned long _lastDrawUpdate = 0; + unsigned long _lastBallUpdate = 0; + unsigned long _lastButtonClick = 0; + + void updateBall(); + void endGame(); + void updateGame(); + uint8_t getPlayerMovement(uint8_t playerId); + void resetLEDs(); + void toggleLed(uint8_t x, uint8_t y, uint8_t type); +}; + +#endif /* PONG_H */ diff --git a/include/render_functions.h b/include/render_functions.h new file mode 100644 index 0000000..154e533 --- /dev/null +++ b/include/render_functions.h @@ -0,0 +1,11 @@ +#ifndef RENDER_FUNCTIONS_H +#define RENDER_FUNCTIONS_H + +#include + +int show_string_on_clock(String message, uint32_t color); +String split(String s, char parser, int index); +String time_to_string(uint8_t hours, uint8_t minutes); +void draw_minute_indicator(uint8_t minutes, uint32_t color); + +#endif /* RENDER_FUNCTIONS_H */ diff --git a/include/snake.h b/include/snake.h new file mode 100644 index 0000000..9cf5f74 --- /dev/null +++ b/include/snake.h @@ -0,0 +1,94 @@ +/** + * @file snake.h + * @author techniccontroller (mail[at]techniccontroller.com) + * @brief Class declaration 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 + * + */ +#ifndef snake_h +#define snake_h + +#include +#include "led_matrix.h" +#include "udp_logger.h" + +#ifdef DEBOUNCE_TIME +#undef DEBOUNCE_TIME +#endif +#define DEBOUNCE_TIME 250 // in ms + +#define X_MAX 11 +#define Y_MAX 11 + +#ifdef GAME_DELAY +#undef GAME_DELAY +#endif +#define GAME_DELAY 400 // in ms + +#define LED_TYPE_SNAKE 1 +#ifdef LED_TYPE_OFF +#undef LED_TYPE_OFF +#endif +#define LED_TYPE_OFF 2 +#define LED_TYPE_FOOD 3 +#define LED_TYPE_BLOOD 4 + +#define DIRECTION_NONE 0 +#define DIRECTION_UP 1 +#define DIRECTION_DOWN 2 +#define DIRECTION_LEFT 3 +#define DIRECTION_RIGHT 4 + +#define GAME_STATE_RUNNING 1 +#define GAME_STATE_END 2 +#define GAME_STATE_INIT 3 + +#define MAX_TAIL_LENGTH (X_MAX * Y_MAX) +#define MIN_TAIL_LENGTH 3 + +class Snake +{ + + struct Coords + { + int x; + int y; + }; + +public: + Snake(); + Snake(LEDMatrix * matrix, UDPLogger * logger); + void loopCycle(); + void initGame(); + void ctrlUp(); + void ctrlDown(); + void ctrlLeft(); + void ctrlRight(); + +private: + LEDMatrix * _ledmatrix; + UDPLogger * _logger; + uint8_t _userDirection = 0; + uint8_t _gameState = 0; + Coords _head = {0, 0}; + Coords _tail[MAX_TAIL_LENGTH] = {0, 0}; + Coords _food = {0, 0}; + unsigned long _lastDrawUpdate = 0; + unsigned long _lastButtonClick = 0; + unsigned int _wormLength = 0; + + void resetLEDs(); + void updateGame(); + void endGame(); + void updateTail(); + void updateFood(); + bool isCollision(); + void toggleLed(uint8_t x, uint8_t y, uint8_t type); +}; + +#endif \ No newline at end of file diff --git a/include/tetris.h b/include/tetris.h new file mode 100644 index 0000000..6e7c637 --- /dev/null +++ b/include/tetris.h @@ -0,0 +1,199 @@ +/** + * @file tetris.h + * @author techniccontroller (mail[at]techniccontroller.com) + * @brief Class definition 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 + * + */ +#ifndef TETRIS_H +#define TETRIS_H + +#include +#include "led_matrix.h" +#include "udp_logger.h" + +#ifdef DEBOUNCE_TIME +#undef DEBOUNCE_TIME +#endif +#define DEBOUNCE_TIME 100 + +#define RED_END_TIME 1500 +#define GAME_STATE_RUNNING 1 +#define GAME_STATE_END 2 +#define GAME_STATE_INIT 3 +#define GAME_STATE_PAUSED 4 +#define GAME_STATE_READY 5 + +// common +#define DIR_UP 1 +#define DIR_DOWN 2 +#define DIR_LEFT 3 +#define DIR_RIGHT 4 +// Maximum size of bricks. Individual bricks can still be smaller (eg 3x3) +#define GREEN 0x008000 +#define RED 0xFF0000 +#define BLUE 0x0000FF +#define YELLOW 0xFFFF00 +#define CHOCOLATE 0xD2691E +#define PURPLE 0xFF00FF +#define WHITE 0XFFFFFF +#define AQUA 0x00FFFF +#define HOTPINK 0xFF1493 +#define DARKORANGE 0xFF8C00 + +#define MAX_BRICK_SIZE 4 +#define BRICKOFFSET -1 // Y offset for new bricks + +#define INIT_SPEED 800 // Initial delay in ms between brick drops +#define SPEED_STEP 10 // Factor for speed increase between levels, default 10 +#define LEVELUP 4 // Number of rows before levelup, default 5 + +#define WIDTH 11 +#define HEIGHT 11 + +class Tetris +{ + + // Playing field + struct Field + { + uint8_t pix[MATRIX_WIDTH][MATRIX_HEIGHT + 1]; // Make field one larger so that collision detection with bottom of field can be done in a uniform way + uint32_t color[MATRIX_WIDTH][MATRIX_HEIGHT]; + }; + + // Structure to represent active brick on screen + struct Brick + { + boolean enabled; // Brick is disabled when it has landed + int xpos, ypos; + + int yOffset; // Y-offset to use when placing brick at top of field + uint8_t siz; + uint8_t pix[MAX_BRICK_SIZE][MAX_BRICK_SIZE]; + + uint32_t col; + }; + + // Struct to contain the different choices of blocks + struct AbstractBrick + { + int yOffset; // Y-offset to use when placing brick at top of field + uint8_t siz; + uint8_t pix[MAX_BRICK_SIZE][MAX_BRICK_SIZE]; + uint32_t col; + }; + +public: + Tetris(); + Tetris(LEDMatrix *myledmatrix, UDPLogger *mylogger); + + void ctrlStart(); + void ctrlPlayPause(); + void ctrlRight(); + void ctrlLeft(); + void ctrlUp(); + void ctrlDown(); + void setSpeed(int32_t i); + + void loopCycle(); + +private: + void resetLEDs(); + void tetrisInit(); + void printField(); + + /* *** Game functions *** */ + void newActiveBrick(); + boolean checkFieldCollision(struct Brick *brick); + boolean checkSidesCollision(struct Brick *brick); + void rotateActiveBrick(); + void shiftActiveBrick(int dir); + void addActiveBrickToField(); + void moveFieldDownOne(uint8_t startRow); + void checkFullLines(); + + void clearField(); + void everythingRed(); + void showscore(); + + LEDMatrix *_ledmatrix; + UDPLogger *_logger; + Brick _activeBrick = {0}; + Field _field; + + bool _allowdrop = false; + bool _tetrisGameOver = false; + int _gameState = GAME_STATE_INIT; + int _score = 0; + unsigned int _speedtetris = 80; + unsigned long _brickSpeed = 0; + unsigned long _droptime = 0; + unsigned long _lastButtonClick = 0; + unsigned long _lastButtonClickr = 0; + unsigned long _nbRowsThisLevel = 0; + unsigned long _nbRowsTotal = 0; + unsigned long _prevUpdateTime = 0; + unsigned long _tetrisshowscore = 0; + + // color library + uint32_t _colorLib[10] = {RED, GREEN, BLUE, YELLOW, CHOCOLATE, PURPLE, WHITE, AQUA, HOTPINK, DARKORANGE}; + + // Brick "library" + AbstractBrick _brickLib[7] = { + {1, // yoffset when adding brick to field + 4, + {{0, 0, 0, 0}, + {0, 1, 1, 0}, + {0, 1, 1, 0}, + {0, 0, 0, 0}}, + WHITE}, + {0, + 4, + {{0, 1, 0, 0}, + {0, 1, 0, 0}, + {0, 1, 0, 0}, + {0, 1, 0, 0}}, + GREEN}, + {1, + 3, + {{0, 0, 0, 0}, + {1, 1, 1, 0}, + {0, 0, 1, 0}, + {0, 0, 0, 0}}, + BLUE}, + {1, + 3, + {{0, 0, 1, 0}, + {1, 1, 1, 0}, + {0, 0, 0, 0}, + {0, 0, 0, 0}}, + YELLOW}, + {1, + 3, + {{0, 0, 0, 0}, + {1, 1, 1, 0}, + {0, 1, 0, 0}, + {0, 0, 0, 0}}, + AQUA}, + {1, + 3, + {{0, 1, 1, 0}, + {1, 1, 0, 0}, + {0, 0, 0, 0}, + {0, 0, 0, 0}}, + HOTPINK}, + {1, + 3, + {{1, 1, 0, 0}, + {0, 1, 1, 0}, + {0, 0, 0, 0}, + {0, 0, 0, 0}}, + RED}}; +}; + +#endif /* TETRIS_H */ \ No newline at end of file diff --git a/include/udp_logger.h b/include/udp_logger.h new file mode 100644 index 0000000..6c2e8d9 --- /dev/null +++ b/include/udp_logger.h @@ -0,0 +1,37 @@ +/** + * @file udp_logger.h + * @author techniccontroller (mail[at]techniccontroller.com) + * @brief Class for sending logging Strings as multicast messages + * @version 0.1 + * @date 2022-03-21 + * + * @copyright Copyright (c) 2022 + * + */ + +#ifndef UDP_LOGGER_H +#define UDP_LOGGER_H + +#include +#include + +class UDPLogger +{ +public: + UDPLogger(); + UDPLogger(IPAddress interface_addr, IPAddress multicast_addr, uint16_t port, String name); + void set_name(String name); + void log_string(String message); + void log_color_24bit(uint32_t color); + +private: + char _packetBuffer[100] = {0}; + int _port; + IPAddress _interfaceAddr; + IPAddress _multicastAddr; + String _name = "Logger"; + unsigned long _lastSend = 0; + WiFiUDP _udp; +}; + +#endif /* UDP_LOGGER_H */ \ No newline at end of file diff --git a/include/wordclock_constants.h b/include/wordclock_constants.h new file mode 100644 index 0000000..cf3f462 --- /dev/null +++ b/include/wordclock_constants.h @@ -0,0 +1,79 @@ +#ifndef WORDCLOCK_CONSTANTS_H +#define WORDCLOCK_CONSTANTS_H + +#include + +// ---------------------------------------------------------------------------------- +// CONSTANTS +// ---------------------------------------------------------------------------------- +#define AP_SSID "WordclockAP" // SSID name of Access Point +#define NTP_SERVER_URL "de.pool.ntp.org" // NTP server address +#define MY_TZ "CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00" // Timezone + +#define HOSTNAME (String("wordclock")) // Local hostname +#define LOGGER_MULTICAST_IP (IPAddress(230, 120, 10, 2)) // IP for UDP server +#define LOGGER_MULTICAST_PORT (8123) // Port for UDP server +#define HTTP_PORT (80) // Standard HTTP port + +// ESP8266 Pins +#define FASTLED_PIN (0) // pin to which the LEDs are attached +#define BUTTON_PIN (5) // pin to which the button is attached + +// Time limits +#define HOUR_MAX (23) +#define MINUTE_MAX (59) + +#define MINUTES_IN_HOUR (60) +#define HOURS_IN_DAY (24) + +// Night mode +#define NIGHTMODE_START_HR (23) +#define NIGHTMODE_START_MIN (0) +#define NIGHTMODE_END_HR (7) +#define NIGHTMODE_END_MIN (0) + +// Timings in us +#define PERIOD_ANIMATION_US (200 * 1000) // 200ms +#define PERIOD_CLOCK_UPDATE_US (1 * 1000 * 1000) // Must be 1s! Do not change! +#define PERIOD_HEARTBEAT_US (1 * 1000 * 1000) // 1s +#define PERIOD_MATRIX_UPDATE_US (33 * 1000) // 33ms +#define PERIOD_NIGHTMODE_CHECK_US (30 * 1000 * 1000) // 30s +#define PERIOD_TIME_UPDATE_US (1 * 1000 * 1000) // 1000ms +#define PERIOD_PONG_US (10 * 1000) // 10ms +#define PERIOD_SNAKE_US (50 * 1000) // 50ms +#define PERIOD_STATE_CHANGE_US (10 * 1000 * 1000) // 10s +#define PERIOD_TETRIS_US (50 * 1000) // 50ms +#define TIMEOUT_LEDDIRECT_US (5 * 1000 * 1000) // 5s +#define PERIOD_BRIGHTNESS_UPDATE_US (5 * 60 * 1000 * 1000) // 300s + +#define SHORT_PRESS_US (100 * 1000) // 100ms +#define LONG_PRESS_US (2 * 1000 * 1000) // 2s +#define VERY_LONG_PRESS_US (10 * 1000 * 1000) // 10s + +// Current limit +#define CURRENT_LIMIT_LED (2500) // limit the total current consumed by LEDs (mA) + +// Brightness ranges range: 0 - 255 +#define DEFAULT_BRIGHTNESS (40) +#define MIN_BRIGHTNESS (10) +#define MAX_BRIGHTNESS UINT8_MAX + +// LED smoothing +#define DEFAULT_SMOOTHING_FACTOR (0.5f) + +// Number of colors in colors array +#define NUM_COLORS (7) +#define COLOR_ORDER GRB // WS2812B color order + +// LED matrix size +#define MATRIX_WIDTH (11) +#define MATRIX_HEIGHT (11) +#define NUM_MATRIX (MATRIX_WIDTH * (MATRIX_HEIGHT + 1)) + +// NTP macros TODO +#define BUILD_YEAR (__DATE__ + 7) // Will expand to current year at compile time as string. +#define NTP_MINIMUM_RX_YEAR (atoi(BUILD_YEAR) - 1) // Will expand to current year minus one at compile time. +#define NTP_START_YEAR (1900) // NTP minimum year is 1900 +#define NTP_MAX_OFFLINE_TIME_S (7 * 24 * 3600) // Watchdog value, maximum offline time before a restart is triggered + +#endif /* WORDCLOCK_CONSTANTS_H */ diff --git a/include/wordclock_esp32.h b/include/wordclock_esp32.h new file mode 100644 index 0000000..dae71df --- /dev/null +++ b/include/wordclock_esp32.h @@ -0,0 +1,89 @@ +#ifndef WORDCLOCK_ESP8266_H +#define WORDCLOCK_ESP8266_H + +#include +#include +#include +#include "led_matrix.h" +#include "udp_logger.h" + +#define RANGE_LIMIT(X, MIN, MAX) (((X) < (MIN)) ? (MIN) : (((X) > (MAX)) ? (MAX) : (X))) +#define RANGE_LIMIT_SUB(X, MIN, MAX, SUB) (((X) < (MIN)) ? (SUB) : (((X) > (MAX)) ? (SUB) : (X))) + +#define EEPROM_SIZE (sizeof(EepromLayout_st) / sizeof(uint8_t)) + +// ---------------------------------------------------------------------------------- +// TYPEDEFS +// ---------------------------------------------------------------------------------- +typedef struct +{ + int start_hour; + int start_min; + int end_hour; + int end_min; +} NightModeTimes_st; + +typedef struct +{ + uint8_t red; + uint8_t green; + uint8_t blue; + uint8_t alpha; // note: unused +} Color_st; + +typedef struct +{ + uint8_t static_brightness; // user-controlled static brightness of LEDs + uint8_t dyn_brightness_min; // user-controlled min brightness of LEDs + uint8_t dyn_brightness_max; // user-controlled max brightness of LEDs + bool flg_dynamic_brightness; // flag if user wants to use daytime dynamic brightness +} Brightness_st; + +typedef struct +{ + NightModeTimes_st night_mode_times; + Brightness_st brightness_values; + Color_st color_values; +} EepromLayout_st; + +typedef enum +{ + ST_CLOCK, + ST_DICLOCK, + ST_SPIRAL, + ST_TETRIS, + ST_SNAKE, + ST_PINGPONG, + ST_HEARTS, + NUM_STATES +} ClockState_en; + +// ---------------------------------------------------------------------------------- +// FUNCTIONS DECLARATIONS +// ---------------------------------------------------------------------------------- +bool check_wifi_status(void); +String leading_zero2digit(int value); +uint8_t calculate_dynamic_brightness(uint8_t min_brightness, uint8_t max_brightness, int hours, int minutes, bool summertime); +uint8_t update_brightness(void); +void check_night_mode(void); +void cold_start_setup(void); +void draw_main_color(void); +void handle_button(void); +void handle_command(void); +void handle_current_state(void); +void handle_data_request(void); +void handle_led_direct(void); +void limit_value_ranges(void); +void log_data(void); +void on_state_entry(uint8_t state); +void read_settings_from_EEPROM(void); +void reset_wifi_credentials(void); +void send_heartbeat(void); +void set_dynamic_brightness(bool state); +void set_main_color(uint8_t red, uint8_t green, uint8_t blue); +void set_night_mode(bool on); +void state_change(ClockState_en new_state); +void update_matrix(void); +void write_settings_to_EEPROM(void); + +#endif /* WORDCLOCK_ESP8266_H */ diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..5887cf7 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,30 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[platformio] +default_envs = az-delivery-devkit-v4_ota + +[env] +platform = espressif32 +board = az-delivery-devkit-v4 +framework = arduino +lib_deps = + densaugeo/base64@^1.4.0 + fastled/FastLED@^3.9.2 + marcmerlin/FastLED NeoMatrix@^1.2 + tzapu/WiFiManager@^2.0.17 + +[env:az-delivery-devkit-v4] +monitor_speed = 115200 +monitor_filters = direct + +[env:az-delivery-devkit-v4_ota] +upload_protocol = espota +upload_port = 192.168.178.xxx diff --git a/res/frontplate/WordClock_DrillingTemplate.pdf b/res/frontplate/WordClock_DrillingTemplate.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ccf9f3ede005c34bc664c926d16cfe38f2514cc4 GIT binary patch literal 32838 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 literal 0 HcmV?d00001 diff --git a/res/frontplate/WordClock_DrillingTemplate.svg b/res/frontplate/WordClock_DrillingTemplate.svg new file mode 100644 index 0000000..0ec7ec6 --- /dev/null +++ b/res/frontplate/WordClock_DrillingTemplate.svg @@ -0,0 +1,1828 @@ + + + + diff --git a/res/frontplate/WordClock_DrillingTemplate_Overlay.svg b/res/frontplate/WordClock_DrillingTemplate_Overlay.svg new file mode 100644 index 0000000..aa2dccf --- /dev/null +++ b/res/frontplate/WordClock_DrillingTemplate_Overlay.svg @@ -0,0 +1,1828 @@ + + + +ERSISTNFÜNFVIERTELZEHNZWANZIGHVORPIKACHUNACHHALBMELFÜNFMITTERNACHTEINSUWUZWEIDREIFUNVIERSECHSOBACHTSIEBENZWÖLFZEHNEUNEUHR diff --git a/res/frontplate/WordClock_Front.svg b/res/frontplate/WordClock_Front.svg new file mode 100644 index 0000000..93bcf60 --- /dev/null +++ b/res/frontplate/WordClock_Front.svg @@ -0,0 +1,1301 @@ + + + + + + + + + + E + R + S + I + S + T + N + F + Ü + N + F + V + I + E + R + T + E + L + Z + E + H + N + Z + W + A + N + Z + I + G + H + V + O + R + P + I + K + A + C + H + U + N + A + C + H + H + A + L + B + M + E + L + F + Ü + N + F + M + I + T + T + E + R + N + A + C + H + T + E + I + N + S + U + W + U + Z + W + E + I + D + R + E + I + F + U + N + V + I + E + R + S + E + C + H + S + O + B + A + C + H + T + S + I + E + B + E + N + Z + W + Ö + L + F + Z + E + H + N + E + U + N + E + U + H + R + + + + + + + + + diff --git a/res/frontplate/WordClock_Front_Paths.svg b/res/frontplate/WordClock_Front_Paths.svg new file mode 100644 index 0000000..f6b08dd --- /dev/null +++ b/res/frontplate/WordClock_Front_Paths.svg @@ -0,0 +1,775 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/frontplate/WordClock_Front_Paths_Inkscape.svg b/res/frontplate/WordClock_Front_Paths_Inkscape.svg new file mode 100644 index 0000000..f2e6874 --- /dev/null +++ b/res/frontplate/WordClock_Front_Paths_Inkscape.svg @@ -0,0 +1,808 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/frontplate/original/frontplate_wordclock2.0_english.svg b/res/frontplate/original/frontplate_wordclock2.0_english.svg new file mode 100644 index 0000000..9804675 --- /dev/null +++ b/res/frontplate/original/frontplate_wordclock2.0_english.svg @@ -0,0 +1,1912 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + I + P + T + I + S + K + T + E + N + N + P + Q + U + A + R + T + E + R + H + A + L + F + T + W + E + N + T + Y + U + F + I + V + E + M + I + N + U + T + E + S + N + A + T + O + P + A + S + T + M + E + A + O + N + E + F + T + W + O + N + T + H + R + E + E + L + R + F + O + U + R + E + A + W + F + I + V + E + O + S + I + X + Z + U + S + E + V + E + N + E + I + G + H + T + E + L + E + V + E + N + U + N + I + N + E + T + W + E + L + V + E + T + E + N + A + W + O + C + L + O + C + K + + + + + + + + + + + + + + + + + + + + diff --git a/res/frontplate/original/frontplate_wordclock2.0_german.svg b/res/frontplate/original/frontplate_wordclock2.0_german.svg new file mode 100644 index 0000000..3018fa2 --- /dev/null +++ b/res/frontplate/original/frontplate_wordclock2.0_german.svg @@ -0,0 +1,1912 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + E + P + S + I + S + T + Ä + F + Ü + N + F + V + I + E + R + T + E + L + Z + E + H + N + Z + W + A + N + Z + I + G + U + V + O + R + T + E + C + H + N + I + C + N + A + C + H + H + A + L + B + M + E + L + F + Ü + N + F + X + C + O + N + T + R + O + L + L + E + R + E + I + N + S + E + A + W + Z + W + E + I + D + R + E + I + T + U + M + V + I + E + R + S + E + C + H + S + Q + Y + A + C + H + T + S + I + E + B + E + N + Z + W + Ö + L + F + Z + E + H + N + E + U + N + J + U + H + R + + + + + + + + + + + + + + + + + + + + diff --git a/res/frontplate/original/frontplate_wordclock2.0_italian.svg b/res/frontplate/original/frontplate_wordclock2.0_italian.svg new file mode 100644 index 0000000..3d3c9bd --- /dev/null +++ b/res/frontplate/original/frontplate_wordclock2.0_italian.svg @@ -0,0 +1,1912 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + S + N + O + O + R + L + E + B + O + R + E + E' + R + L' + U + N + A + S + D + U + E + Z + T + R + E + O + T + T + O + N + O + V + E + D + I + E + C + I + U + N + D + I + C + I + D + O + D + I + C + I + S + E + T + T + E + Q + U + A + T + T + R + O + C + S + E + I + C + I + N + Q + U + E + A + M + E + N + O + E + C + U + N + O + Q + U + A + R + T + O + V + E + N + T + I + C + I + N + Q + U + E + L + V + E + T + E + N + A + W + O + C + L + D + I + E + C + I + P + M + E + Z + Z + A + + + + + + + + + + + + + + + + + + + + diff --git a/res/webserver/fs.html b/res/webserver/fs.html new file mode 100644 index 0000000..7038ebc --- /dev/null +++ b/res/webserver/fs.html @@ -0,0 +1,77 @@ + + + + + + + + Filesystem Manager + + + +

ESP32 Filesystem Manager

+
+ + +
+
+ + +
+
+
+ +
+ + diff --git a/res/webserver/icons/all_icons.svg b/res/webserver/icons/all_icons.svg new file mode 100644 index 0000000..db3df32 --- /dev/null +++ b/res/webserver/icons/all_icons.svg @@ -0,0 +1,273 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + 34 + 12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/webserver/icons/arrow_left.svg b/res/webserver/icons/arrow_left.svg new file mode 100644 index 0000000..07fc109 --- /dev/null +++ b/res/webserver/icons/arrow_left.svg @@ -0,0 +1,15 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/res/webserver/icons/arrow_right.svg b/res/webserver/icons/arrow_right.svg new file mode 100644 index 0000000..d862ef5 --- /dev/null +++ b/res/webserver/icons/arrow_right.svg @@ -0,0 +1,15 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/res/webserver/icons/clock.svg b/res/webserver/icons/clock.svg new file mode 100644 index 0000000..0712a28 --- /dev/null +++ b/res/webserver/icons/clock.svg @@ -0,0 +1,25 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/res/webserver/icons/diclock.svg b/res/webserver/icons/diclock.svg new file mode 100644 index 0000000..4a5d25a --- /dev/null +++ b/res/webserver/icons/diclock.svg @@ -0,0 +1,20 @@ + + + + + + image/svg+xml + + + + + + + + + 34 + 12 + + + + diff --git a/res/webserver/icons/hearts.svg b/res/webserver/icons/hearts.svg new file mode 100644 index 0000000..87e1fb3 --- /dev/null +++ b/res/webserver/icons/hearts.svg @@ -0,0 +1,71 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/res/webserver/icons/pause.svg b/res/webserver/icons/pause.svg new file mode 100644 index 0000000..4a2c9d4 --- /dev/null +++ b/res/webserver/icons/pause.svg @@ -0,0 +1,16 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/res/webserver/icons/pingpong.svg b/res/webserver/icons/pingpong.svg new file mode 100644 index 0000000..f0aa1dd --- /dev/null +++ b/res/webserver/icons/pingpong.svg @@ -0,0 +1,23 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/res/webserver/icons/play.svg b/res/webserver/icons/play.svg new file mode 100644 index 0000000..7a02675 --- /dev/null +++ b/res/webserver/icons/play.svg @@ -0,0 +1,15 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/res/webserver/icons/playpause.svg b/res/webserver/icons/playpause.svg new file mode 100644 index 0000000..0e9db2c --- /dev/null +++ b/res/webserver/icons/playpause.svg @@ -0,0 +1,20 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/res/webserver/icons/refresh.svg b/res/webserver/icons/refresh.svg new file mode 100644 index 0000000..c4131d5 --- /dev/null +++ b/res/webserver/icons/refresh.svg @@ -0,0 +1,18 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/res/webserver/icons/settings.svg b/res/webserver/icons/settings.svg new file mode 100644 index 0000000..2420243 --- /dev/null +++ b/res/webserver/icons/settings.svg @@ -0,0 +1,29 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/webserver/icons/snake.svg b/res/webserver/icons/snake.svg new file mode 100644 index 0000000..40e4c80 --- /dev/null +++ b/res/webserver/icons/snake.svg @@ -0,0 +1,22 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/res/webserver/icons/spiral.svg b/res/webserver/icons/spiral.svg new file mode 100644 index 0000000..c7479fc --- /dev/null +++ b/res/webserver/icons/spiral.svg @@ -0,0 +1,19 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/res/webserver/icons/tetris.svg b/res/webserver/icons/tetris.svg new file mode 100644 index 0000000..4d4ae65 --- /dev/null +++ b/res/webserver/icons/tetris.svg @@ -0,0 +1,24 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/res/webserver/index.html b/res/webserver/index.html new file mode 100644 index 0000000..70f42ef --- /dev/null +++ b/res/webserver/index.html @@ -0,0 +1,702 @@ + + + + + + + + WORDCLOCK 2.0 + + + + +
+ +

WORDCLOCK 2.0

+ +
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
SAVE
+
+ +
+
+
+ MODE +
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/webserver/style32.css b/res/webserver/style32.css new file mode 100644 index 0000000..9c97704 --- /dev/null +++ b/res/webserver/style32.css @@ -0,0 +1,108 @@ + +/*For more information visit: https://fipsok.de*/ +body { + font-family: sans-serif; + background-color: #a9a9a9; + display: flex; + flex-flow: column; + align-items: center; +} +h1,h2 { + color: #e1e1e1; + text-shadow: 2px 2px 2px black; +} +li { + background-color: #7cfc00; + list-style-type: none; + margin-bottom: 10px; + padding-right: 5px; + border-top: 3px solid #7cfc00; + border-bottom: 3px solid #7cfc00; + box-shadow: 5px 5px 5px #000000b3; +} +li a:first-child, li b { + background-color: #ff4500; + font-weight: bold; + color: white; + text-decoration: none; + padding: 0 5px; + border-top: 3.2px solid #ff4500; + border-bottom: 3.6px solid #ff4500; + text-shadow: 2px 2px 1px black; + cursor:pointer; +} +li strong { + color: red; +} +input { + height: 35px; + font-size: 13px; +} +h1+main { + display: flex; +} +aside { + display: flex; + flex-direction: column; + padding: 0.2em; +} +#left { + align-items: flex-end; + text-shadow: 1px 1px 2px #757474; +} +.note { + background-color: salmon; + padding: 0.5em; + margin-top: 1em; + text-align: center; + max-width: 320px; + border-radius: 0.5em; +} +nav { + display: flex; + align-items: baseline; + justify-content: space-between; +} +.no { + display: none; +} +#cr { + font-weight: bold; + cursor:pointer; + font-size: 1.5em; +} +#up { + width: auto; +} +button { + width: 130px; + height: 40px; + font-size: 16px; + margin-top: 1em; + cursor: pointer; + box-shadow: 5px 5px 5px #000000b3; +} +div button { + background-color: #adff2f; +} +form [title] { + background-color: silver; + font-size: 16px; + width: 125px; +} +form:nth-of-type(2) { + margin-bottom: 1em; +} +[name="group"] { + display: none; +} +[name="group"] + label { + font-size: 1.5em; + margin-right: 5px; +} +[name="group"] + label::before { + content: "\002610"; +} +[name="group"]:checked + label::before { + content: '\002611\0027A5'; +} diff --git a/scripts/http_diagnosis.py b/scripts/http_diagnosis.py new file mode 100644 index 0000000..95fdb22 --- /dev/null +++ b/scripts/http_diagnosis.py @@ -0,0 +1,13 @@ +import requests + +r = requests.get("http://wordclock.local/cmd?diag=reset_info") +print(r.status_code) +print(r.text) + +r = requests.get("http://wordclock.local/cmd?diag=sketch_info") +print(r.status_code) +print(r.text) + +r = requests.get("http://wordclock.local/cmd?diag=device_info") +print(r.status_code) +print(r.text) \ No newline at end of file diff --git a/scripts/multicastUDP_receiver.py b/scripts/multicastUDP_receiver.py new file mode 100644 index 0000000..2c611f9 --- /dev/null +++ b/scripts/multicastUDP_receiver.py @@ -0,0 +1,52 @@ +from datetime import datetime +import logging +import queue +import socket +import struct +import sys +from pathlib import Path + +LOG_PATH = Path("C:/temp/wordclock_log.txt") + +def setup_logging(): + FORMAT = "%(asctime)s %(message)s" + logging.basicConfig(format=FORMAT, level=logging.INFO) + logger = logging.getLogger() + handler = logging.FileHandler(LOG_PATH) + handler.setLevel(logging.INFO) + logger.addHandler(handler) + return logger + + +def get_ip_address(): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("1.1.1.1", 80)) + return s.getsockname()[0] + + +logger = setup_logging() + +multicast_group = "230.120.10.2" +server_address = ("", 8123) + +# Create the socket +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + +# Bind to the server address +sock.bind(server_address) + +logging.info("Start") + +# Tell the operating system to add the socket to the multicast group +# on all interfaces. +group = socket.inet_aton(multicast_group) +mreq = struct.pack("4s4s", group, socket.inet_aton(get_ip_address())) +sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) + +logger.info("Ready") + +# Receive/respond loop +while True: + data, address = sock.recvfrom(1024) + data_str = data.decode("utf-8").strip() + logger.info(data_str) diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..361bb3382c80d5b22da282e11940b81a708aad69 GIT binary patch literal 184 zcmXwzOA3QP5JcZv@FzO?7K%s$q(%*K-~X9V;6Hz4X!qo>DWlT8m8c_fmX@NspgjMu1YWL+EJ`{-?U!vw%yxB OADvyxzkX%ZKluk<9~>J1 literal 0 HcmV?d00001 diff --git a/src/connectivity/diagnosis.cpp b/src/connectivity/diagnosis.cpp new file mode 100644 index 0000000..4ae4e2f --- /dev/null +++ b/src/connectivity/diagnosis.cpp @@ -0,0 +1,87 @@ +#include "diagnosis.h" + + +Diagnosis::Diagnosis(UDPLogger *logger, LEDMatrix *matrix) // constructor +{ + _logger = logger; + _matrix = matrix; +} + +String Diagnosis::handle_command(const String &command) +{ + if (command == "device_info") + { + return print_device_info(); + } + else if (command == "sketch_info") + { + return print_sketch_info(); + } + else if (command == "reset_info") + { + return print_last_reset_details(); + } + else if (command == "matrix_fps") + { + return print_matrix_fps(); + } + else + { + // Handle unknown command + String unknown_command = "Diagnosis: Unknown command!\n"; + print(unknown_command); + return unknown_command; + } +} + +String Diagnosis::print_device_info() +{ + // Retrieve and print device information + String device_info = "Device Information:\n"; + device_info += "Chip ID: " + String(ESP.getChipModel()) + "\n"; + device_info += "Flash Chip Size: " + String(ESP.getFlashChipSize()) + " bytes\n"; + device_info += "Free Heap Size: " + String(ESP.getFreeHeap()) + " bytes\n"; + device_info += "Free Sketch Space: " + String(ESP.getFreeSketchSpace()) + " bytes\n"; + device_info += "SDK Version: " + String(ESP.getSdkVersion()) + "\n"; + print(device_info); + return device_info; +} + +String Diagnosis::print_sketch_info() +{ + // Retrieve and print sketch information + String sketch_info = "Sketch Information:\n"; + sketch_info += "Sketch Size: " + String(ESP.getSketchSize()) + " bytes\n"; + sketch_info += "Sketch MD5: " + String(ESP.getSketchMD5()) + "\n"; + print(sketch_info); + return sketch_info; +} + +String Diagnosis::print_last_reset_details() +{ + // Retrieve and print last reset details + String reset_info = "Last Reset Details:\n"; + reset_info += "Reset Reason: " + String(esp_reset_reason()) + "\n"; + print(reset_info); + return reset_info; +} + +String Diagnosis::print_matrix_fps() +{ + // Retrieve and print matrix FPS + String matrix_fps = "Matrix FPS: " + String(_matrix->get_fps()) + "\n"; + print(matrix_fps); + return matrix_fps; +} + +void Diagnosis::print(const String &s) +{ + if (_logger != nullptr) + { + _logger->log_string(s); + } + else + { + Serial.println(s); + } +} diff --git a/src/connectivity/ota_wrapper.cpp b/src/connectivity/ota_wrapper.cpp new file mode 100644 index 0000000..b17e5e9 --- /dev/null +++ b/src/connectivity/ota_wrapper.cpp @@ -0,0 +1,54 @@ +#include +#include +#include "ota_wrapper.h" + +// setup Arduino OTA +void setupOTA(String hostname) +{ + ArduinoOTA.setHostname(hostname.c_str()); + + 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(); +} \ No newline at end of file diff --git a/src/connectivity/udp_logger.cpp b/src/connectivity/udp_logger.cpp new file mode 100644 index 0000000..5f24894 --- /dev/null +++ b/src/connectivity/udp_logger.cpp @@ -0,0 +1,44 @@ +#include "udp_logger.h" + +UDPLogger::UDPLogger() {} + +UDPLogger::UDPLogger(IPAddress interface_addr, IPAddress multicast_addr, uint16_t port, String name) +{ + _interfaceAddr = interface_addr; + _multicastAddr = multicast_addr; + _name = name; + _port = port; + _udp.beginMulticast(_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 10 milliseconds before + if ((esp_timer_get_time() / 1000) < (_lastSend + 10)) + { + delay(10); + } + message = _name + ": " + message; +#ifdef SERIAL_DEBUG + Serial.println(message); + delay(10); +#endif /* SERIAL_DEBUG */ + _udp.beginMulticast(_multicastAddr, _port); + message.toCharArray(_packetBuffer, 100); + _udp.print(_packetBuffer); + _udp.endPacket(); + _lastSend = (esp_timer_get_time() / 1000); +} + +void UDPLogger::log_color_24bit(uint32_t color) +{ + uint8_t result_red = color >> 16 & UINT8_MAX; + uint8_t result_green = color >> 8 & UINT8_MAX; + uint8_t result_blue = color & UINT8_MAX; + log_string(String(result_red) + ", " + String(result_green) + ", " + String(result_blue)); +} \ No newline at end of file diff --git a/src/games/pong.cpp b/src/games/pong.cpp new file mode 100644 index 0000000..60174f8 --- /dev/null +++ b/src/games/pong.cpp @@ -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 ((esp_timer_get_time() / 1000) > (_lastButtonClick + DEBOUNCE_TIME)) + { + _playerMovement[playerid] = PADDLE_MOVE_DOWN; // need to swap direction as field is rotated 180deg + _lastButtonClick = (esp_timer_get_time() / 1000); + } +} + +/** + * @brief Trigger control: DOWN for given player + * + * @param playerid id of player {0, 1} + */ +void Pong::ctrlDown(uint8_t playerid) +{ + if ((esp_timer_get_time() / 1000) > (_lastButtonClick + DEBOUNCE_TIME)) + { + _playerMovement[playerid] = PADDLE_MOVE_UP; // need to swap direction as field is rotated 180deg + _lastButtonClick = (esp_timer_get_time() / 1000); + } +} + +/** + * @brief Trigger control: NONE for given player + * + * @param playerid id of player {0, 1} + */ +void Pong::ctrlNone(uint8_t playerid) +{ + if ((esp_timer_get_time() / 1000) > (_lastButtonClick + DEBOUNCE_TIME)) + { + _playerMovement[playerid] = PADDLE_MOVE_NONE; + _lastButtonClick = (esp_timer_get_time() / 1000); + } +} + +/** + * @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 = (esp_timer_get_time() / 1000); + + _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 (((esp_timer_get_time() / 1000) - _lastBallUpdate) < _ballDelay) + { + return; + } + _lastBallUpdate = (esp_timer_get_time() / 1000); + 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 (((esp_timer_get_time() / 1000) - _lastDrawUpdate) < GAME_DELAY) + { + return; + } + _lastDrawUpdate = (esp_timer_get_time() / 1000); + + // 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); +} \ No newline at end of file diff --git a/src/games/snake.cpp b/src/games/snake.cpp new file mode 100644 index 0000000..bdd3fe4 --- /dev/null +++ b/src/games/snake.cpp @@ -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 (((esp_timer_get_time() / 1000) > (_lastButtonClick + DEBOUNCE_TIME)) && (_gameState == GAME_STATE_RUNNING) && (_userDirection != DIRECTION_UP)) + { + _logger->log_string("Snake: UP"); + _userDirection = DIRECTION_DOWN; // need to swap direction as field is rotated 180deg + _lastButtonClick = (esp_timer_get_time() / 1000); + } +} + +/** + * @brief Trigger control: DOWN + * + */ +void Snake::ctrlDown() +{ + if (((esp_timer_get_time() / 1000) > (_lastButtonClick + DEBOUNCE_TIME)) && (_gameState == GAME_STATE_RUNNING) && (_userDirection != DIRECTION_DOWN)) + { + _logger->log_string("Snake: DOWN"); + _userDirection = DIRECTION_UP; // need to swap direction as field is rotated 180deg + _lastButtonClick = (esp_timer_get_time() / 1000); + } +} + +/** + * @brief Trigger control: RIGHT + * + */ +void Snake::ctrlRight() +{ + if (((esp_timer_get_time() / 1000) > (_lastButtonClick + DEBOUNCE_TIME)) && (_gameState == GAME_STATE_RUNNING) && (_userDirection != DIRECTION_RIGHT)) + { + _logger->log_string("Snake: RIGHT"); + _userDirection = DIRECTION_LEFT; // need to swap direction as field is rotated 180deg + _lastButtonClick = (esp_timer_get_time() / 1000); + } +} + +/** + * @brief Trigger control: LEFT + * + */ +void Snake::ctrlLeft() +{ + if (((esp_timer_get_time() / 1000) > (_lastButtonClick + DEBOUNCE_TIME)) && (_gameState == GAME_STATE_RUNNING) && (_userDirection != DIRECTION_LEFT)) + { + _logger->log_string("Snake: LEFT"); + _userDirection = DIRECTION_RIGHT; // need to swap direction as field is rotated 180deg + _lastButtonClick = (esp_timer_get_time() / 1000); + } +} + +/** + * @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 = (esp_timer_get_time() / 1000); + + 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 (((esp_timer_get_time() / 1000) - _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 = (esp_timer_get_time() / 1000); + } +} + +/** + * @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); +} \ No newline at end of file diff --git a/src/games/tetris.cpp b/src/games/tetris.cpp new file mode 100644 index 0000000..6b3e89b --- /dev/null +++ b/src/games/tetris.cpp @@ -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; + _gameState = GAME_STATE_READY; +} + +/** + * @brief Run main loop for one cycle + * + */ +void Tetris::loopCycle() +{ + switch (_gameState) + { + 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 ((esp_timer_get_time() / 1000) > (_droptime + 50)) + { + _droptime = (esp_timer_get_time() / 1000); + shiftActiveBrick(DIR_DOWN); + printField(); + } + } + + // move down with regular speed + if (((esp_timer_get_time() / 1000) - _prevUpdateTime) > (_brickSpeed * _speedtetris / 100)) + { + _prevUpdateTime = (esp_timer_get_time() / 1000); + 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 = (esp_timer_get_time() / 1000); // 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 = (esp_timer_get_time() / 1000); + } + + if ((esp_timer_get_time() / 1000) > (_tetrisshowscore + RED_END_TIME)) + { + resetLEDs(); + _score = _nbRowsTotal; + showscore(); + _gameState = GAME_STATE_READY; + } + break; + } +} + +/** + * @brief Trigger control: START (& restart) + * + */ +void Tetris::ctrlStart() +{ + if ((esp_timer_get_time() / 1000) > _lastButtonClick + DEBOUNCE_TIME) + { + _lastButtonClick = (esp_timer_get_time() / 1000); + _gameState = GAME_STATE_INIT; + } +} + +/** + * @brief Trigger control: PAUSE/PLAY + * + */ +void Tetris::ctrlPlayPause() +{ + if ((esp_timer_get_time() / 1000) > _lastButtonClick + DEBOUNCE_TIME) + { + _lastButtonClick = (esp_timer_get_time() / 1000); + if (_gameState == GAME_STATE_PAUSED) + { + _logger->log_string("Tetris: continue"); + + _gameState = GAME_STATE_RUNNING; + } + else if (_gameState == GAME_STATE_RUNNING) + { + _logger->log_string("Tetris: pause"); + + _gameState = GAME_STATE_PAUSED; + } + } +} + +/** + * @brief Trigger control: RIGHT + * + */ +void Tetris::ctrlRight() +{ + if ((esp_timer_get_time() / 1000) > _lastButtonClick + DEBOUNCE_TIME && _gameState == GAME_STATE_RUNNING) + { + _lastButtonClick = (esp_timer_get_time() / 1000); + shiftActiveBrick(DIR_RIGHT); + printField(); + } +} + +/** + * @brief Trigger control: LEFT + * + */ +void Tetris::ctrlLeft() +{ + if ((esp_timer_get_time() / 1000) > _lastButtonClick + DEBOUNCE_TIME && _gameState == GAME_STATE_RUNNING) + { + _lastButtonClick = (esp_timer_get_time() / 1000); + shiftActiveBrick(DIR_LEFT); + printField(); + } +} + +/** + * @brief Trigger control: UP (rotate) + * + */ +void Tetris::ctrlUp() +{ + if ((esp_timer_get_time() / 1000) > _lastButtonClick + DEBOUNCE_TIME && _gameState == GAME_STATE_RUNNING) + { + _lastButtonClick = (esp_timer_get_time() / 1000); + rotateActiveBrick(); + printField(); + } +} + +/** + * @brief Trigger control: DOWN (drop) + * + */ +void Tetris::ctrlDown() +{ + // longer debounce time, to prevent immediate drop + if ((esp_timer_get_time() / 1000) > _lastButtonClickr + DEBOUNCE_TIME * 5 && _gameState == GAME_STATE_RUNNING) + { + _allowdrop = true; + _lastButtonClickr = (esp_timer_get_time() / 1000); + } +} + +/** + * @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 = (esp_timer_get_time() / 1000); + + _gameState = 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; + _gameState = 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(); +} \ No newline at end of file diff --git a/src/matrix/animation_functions.cpp b/src/matrix/animation_functions.cpp new file mode 100644 index 0000000..d6d0984 --- /dev/null +++ b/src/matrix/animation_functions.cpp @@ -0,0 +1,1192 @@ +#include +#include "animation_functions.h" +#include "wordclock_constants.h" +#include "udp_logger.h" +#include "led_matrix.h" + +extern UDPLogger logger; +extern LEDMatrix led_matrix; + +const int8_t dx[] = {1, -1, 0, 0}; +const int8_t dy[] = {0, 0, -1, 1}; + +bool spiral_direction = false; // Direction of sprial animation + +/** + * @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)); + delay(10); + + // 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"); + delay(10); + + 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; +} + +#define HEART_ANIMATION_FRAMES 5 +const uint32_t heart_frames_colormap_11x11[HEART_ANIMATION_FRAMES][MATRIX_WIDTH][MATRIX_HEIGHT] = + { + { + { + 0x005b000a, + 0x002d0304, + 0x000f0002, + 0x00090001, + 0x00350306, + 0x00530103, + 0x00310407, + 0x000a0104, + 0x00090001, + 0x00300306, + 0x00570006, + }, + { + 0x00330206, + 0x00090000, + 0x00ce0404, + 0x00d70300, + 0x000f0008, + 0x00350306, + 0x000b0100, + 0x00c70600, + 0x00d40201, + 0x00080202, + 0x00390006, + }, + { + 0x000a0000, + 0x00db0503, + 0x00940906, + 0x00950a03, + 0x00e20100, + 0x000e0008, + 0x00d00000, + 0x00960806, + 0x00940605, + 0x00d50600, + 0x000d0103, + }, + { + 0x00da010c, + 0x00940700, + 0x00580000, + 0x00570104, + 0x00940a0a, + 0x00d40100, + 0x00940a08, + 0x005a0006, + 0x005b0005, + 0x009d0305, + 0x00da0300, + }, + { + 0x00d70707, + 0x00910a06, + 0x00560202, + 0x00570207, + 0x00580007, + 0x00900a09, + 0x00540109, + 0x005a0004, + 0x005b0007, + 0x00910a04, + 0x00d40201, + }, + { + 0x00da0207, + 0x0092070c, + 0x005b0002, + 0x00330005, + 0x00300405, + 0x00580308, + 0x002e0503, + 0x002c0404, + 0x005a0004, + 0x00970609, + 0x00d80202, + }, + { + 0x00040402, + 0x00dc0600, + 0x008f0a0b, + 0x00590006, + 0x00310504, + 0x00320406, + 0x00330507, + 0x00620008, + 0x00960709, + 0x00dd0301, + 0x000d0304, + }, + { + 0x00330205, + 0x00090100, + 0x00d8020a, + 0x00930906, + 0x00570209, + 0x00330205, + 0x00580007, + 0x00980808, + 0x00d60200, + 0x000d0003, + 0x00340205, + }, + { + 0x00560008, + 0x00340306, + 0x00100001, + 0x00d40a00, + 0x009e0507, + 0x005c0007, + 0x009e0508, + 0x00e50000, + 0x000e0001, + 0x00310305, + 0x00590004, + }, + { + 0x00a00606, + 0x00580007, + 0x00350304, + 0x00100001, + 0x00d40404, + 0x00940902, + 0x00d50407, + 0x00000205, + 0x00350304, + 0x00560108, + 0x00900906, + }, + { + 0x00d90104, + 0x008d080b, + 0x0060000b, + 0x00340106, + 0x00050100, + 0x00e00106, + 0x000f0000, + 0x00370003, + 0x00570104, + 0x00950a07, + 0x00e10007, + }, + }, + { + { + 0x00310304, + 0x000b0001, + 0x00da0308, + 0x00d90101, + 0x000b0001, + 0x00310304, + 0x000b0001, + 0x00d60101, + 0x00d60101, + 0x000b0001, + 0x00310304, + }, + { + 0x000b0001, + 0x00d90101, + 0x00940808, + 0x00940808, + 0x00d60101, + 0x000b0001, + 0x00d60101, + 0x00940808, + 0x00940808, + 0x00d60101, + 0x000b0001, + }, + { + 0x00d90101, + 0x00940808, + 0x00590005, + 0x00590005, + 0x00940808, + 0x00d90101, + 0x00940808, + 0x00560004, + 0x00590005, + 0x00940808, + 0x00d60101, + }, + { + 0x00940808, + 0x00590005, + 0x00370803, + 0x00310304, + 0x00590005, + 0x00940808, + 0x00590005, + 0x00310304, + 0x00310304, + 0x00590005, + 0x00940808, + }, + { + 0x00940808, + 0x00590005, + 0x002c0303, + 0x00310304, + 0x00310304, + 0x00560004, + 0x00310304, + 0x00310304, + 0x00310304, + 0x00590005, + 0x00940808, + }, + { + 0x00940808, + 0x00590005, + 0x00310304, + 0x000b0001, + 0x000b0001, + 0x00310304, + 0x000b0001, + 0x00130102, + 0x00310304, + 0x00590005, + 0x00940808, + }, + { + 0x00d60101, + 0x00940808, + 0x00590005, + 0x00310304, + 0x000b0001, + 0x000b0001, + 0x000b0001, + 0x00310304, + 0x00590005, + 0x00940808, + 0x00d90101, + }, + { + 0x000b0001, + 0x00d90802, + 0x00940808, + 0x00590005, + 0x00310304, + 0x000b0001, + 0x00310304, + 0x00560004, + 0x00940808, + 0x00d90101, + 0x000b0001, + }, + { + 0x00310304, + 0x000b0001, + 0x00d90101, + 0x00940808, + 0x00590005, + 0x00310304, + 0x00590005, + 0x00940808, + 0x00d90802, + 0x000b0001, + 0x00310304, + }, + { + 0x00590005, + 0x00310304, + 0x000b0001, + 0x00d90101, + 0x00940808, + 0x00560004, + 0x00940808, + 0x00d60101, + 0x000b0001, + 0x00310304, + 0x00560004, + }, + { + 0x00940808, + 0x00590005, + 0x00310304, + 0x000b0001, + 0x00d60101, + 0x00940808, + 0x00d60101, + 0x000b0001, + 0x00310304, + 0x00590005, + 0x00940808, + }, + }, + { + { + 0x000b0001, + 0x00d90101, + 0x00940808, + 0x00940808, + 0x00d90101, + 0x000b0001, + 0x00d90101, + 0x00940808, + 0x00940808, + 0x00d90101, + 0x000b0001, + }, + { + 0x00d90101, + 0x00940808, + 0x00590005, + 0x00590005, + 0x00940808, + 0x00d90101, + 0x00940808, + 0x00560004, + 0x00560004, + 0x00940808, + 0x00d90101, + }, + { + 0x00940808, + 0x00590005, + 0x002d0303, + 0x00320508, + 0x00590005, + 0x00940808, + 0x00590005, + 0x002d0303, + 0x00310304, + 0x00560004, + 0x00940808, + }, + { + 0x00560004, + 0x00320508, + 0x000b0001, + 0x000b0001, + 0x00310304, + 0x00590005, + 0x002f0308, + 0x000b0001, + 0x000b0001, + 0x002d0303, + 0x00590005, + }, + { + 0x00560004, + 0x002d0303, + 0x000b0001, + 0x000b0001, + 0x000b0001, + 0x00320508, + 0x000b0001, + 0x000b0001, + 0x000b0001, + 0x002d0303, + 0x00590005, + }, + { + 0x00590005, + 0x00310304, + 0x000b0001, + 0x00d40809, + 0x00d90101, + 0x000b0001, + 0x00d90101, + 0x00d90101, + 0x000b0001, + 0x003b0203, + 0x00590005, + }, + { + 0x00940808, + 0x00590005, + 0x002d0303, + 0x000b0001, + 0x00d90101, + 0x00d40809, + 0x00d90101, + 0x000b0001, + 0x002d0303, + 0x00590005, + 0x00940808, + }, + { + 0x00d40809, + 0x00940808, + 0x00590005, + 0x00310304, + 0x000b0001, + 0x00d90101, + 0x000b0001, + 0x00320508, + 0x00590005, + 0x00940808, + 0x00d90101, + }, + { + 0x000b0001, + 0x00d60101, + 0x00940808, + 0x005a0209, + 0x002d0303, + 0x000b0001, + 0x00310304, + 0x00560004, + 0x00940808, + 0x00d90101, + 0x000b0001, + }, + { + 0x00310304, + 0x000b0001, + 0x00d90101, + 0x00940808, + 0x00590005, + 0x00310304, + 0x00590005, + 0x00940808, + 0x00d60101, + 0x000b0001, + 0x00310304, + }, + { + 0x00560004, + 0x00310304, + 0x000b0001, + 0x00d90101, + 0x00940808, + 0x00590005, + 0x00940808, + 0x00d60101, + 0x000b0001, + 0x00310304, + 0x00590005, + }, + }, + { + { + 0x00d80802, + 0x00940808, + 0x00590004, + 0x00590004, + 0x00940808, + 0x00d90101, + 0x00940808, + 0x00590004, + 0x00590004, + 0x00940808, + 0x00d90101, + }, + { + 0x00940808, + 0x00560004, + 0x00310304, + 0x00310304, + 0x00560004, + 0x00940808, + 0x00590004, + 0x002a0309, + 0x00310304, + 0x00590004, + 0x00940808, + }, + { + 0x00590004, + 0x00310304, + 0x000b0001, + 0x000b0001, + 0x00310304, + 0x00590004, + 0x00310304, + 0x000b0001, + 0x000b0001, + 0x00310304, + 0x00560004, + }, + { + 0x00350408, + 0x000b0001, + 0x00d80802, + 0x00d60101, + 0x000b0001, + 0x00310304, + 0x000b0001, + 0x00d60101, + 0x00d60101, + 0x000b0001, + 0x00310304, + }, + { + 0x002c0303, + 0x00130102, + 0x00d60101, + 0x00d60101, + 0x00d80802, + 0x000b0001, + 0x00d90101, + 0x00d60101, + 0x00d80802, + 0x000b0001, + 0x00310304, + }, + { + 0x00310304, + 0x000b0001, + 0x00d90101, + 0x00940808, + 0x00940808, + 0x00db0209, + 0x00940808, + 0x00940808, + 0x00d60101, + 0x000b0001, + 0x00310304, + }, + { + 0x00590004, + 0x00310304, + 0x000b0001, + 0x00d80802, + 0x00940808, + 0x00940808, + 0x00940808, + 0x00d90101, + 0x000b0001, + 0x003b0303, + 0x00560004, + }, + { + 0x00940808, + 0x00590004, + 0x00350408, + 0x000b0001, + 0x00d90101, + 0x00940808, + 0x00d90101, + 0x000b0001, + 0x00310304, + 0x00590004, + 0x00940808, + }, + { + 0x00d90101, + 0x00940808, + 0x00590004, + 0x002e0804, + 0x00130102, + 0x00d60101, + 0x000b0001, + 0x00310304, + 0x00590004, + 0x00940808, + 0x00d90101, + }, + { + 0x000b0001, + 0x00e10102, + 0x00940808, + 0x00590004, + 0x002c0303, + 0x000b0001, + 0x003b0303, + 0x00590004, + 0x00940808, + 0x00d90101, + 0x000b0001, + }, + { + 0x00310304, + 0x000b0001, + 0x00d80802, + 0x00940808, + 0x00560004, + 0x00310304, + 0x00590004, + 0x00940808, + 0x00d90101, + 0x000b0001, + 0x00310304, + }, + }, + { + { + 0x00940808, + 0x00590005, + 0x00310304, + 0x00310304, + 0x00560004, + 0x00940808, + 0x00590005, + 0x002c0303, + 0x00310304, + 0x00560004, + 0x00940808, + }, + { + 0x00560004, + 0x00310304, + 0x000b0001, + 0x000b0001, + 0x00310304, + 0x00590005, + 0x00310304, + 0x000b0001, + 0x000b0001, + 0x00310304, + 0x00590005, + }, + { + 0x00310304, + 0x000b0001, + 0x00d90101, + 0x00d90101, + 0x000b0001, + 0x002c0303, + 0x000b0001, + 0x00d90101, + 0x00cf0804, + 0x000b0001, + 0x00310304, + }, + { + 0x000b0001, + 0x00d60101, + 0x00940808, + 0x00940808, + 0x00e10102, + 0x000b0001, + 0x00d90101, + 0x00940808, + 0x00940808, + 0x00d90101, + 0x000b0001, + }, + { + 0x000b0001, + 0x00d90101, + 0x00940808, + 0x00940808, + 0x00940808, + 0x00d90101, + 0x00940808, + 0x00940808, + 0x00940808, + 0x00cf0804, + 0x000b0001, + }, + { + 0x000b0001, + 0x00da0308, + 0x00940808, + 0x00590005, + 0x00590005, + 0x00940808, + 0x00590005, + 0x00590005, + 0x00940808, + 0x00d90101, + 0x000b0001, + }, + { + 0x00310304, + 0x000b0001, + 0x00d90101, + 0x00940808, + 0x00590005, + 0x00590005, + 0x00560004, + 0x00940808, + 0x00d90101, + 0x000b0001, + 0x00310304, + }, + { + 0x005a0109, + 0x00310304, + 0x000b0001, + 0x00d90101, + 0x00940808, + 0x00560004, + 0x00940808, + 0x00cf0804, + 0x000b0001, + 0x00310304, + 0x00590005, + }, + { + 0x00940808, + 0x00590005, + 0x00310304, + 0x000b0001, + 0x00e10102, + 0x00940808, + 0x00d60101, + 0x000b0001, + 0x00310304, + 0x00590005, + 0x00940808, + }, + { + 0x00d90101, + 0x00940808, + 0x00590005, + 0x00310304, + 0x000b0001, + 0x00d90101, + 0x00130102, + 0x00310304, + 0x00590005, + 0x00940808, + 0x00e10102, + }, + { + 0x000b0001, + 0x00d90101, + 0x00940808, + 0x00590005, + 0x00310304, + 0x000b0001, + 0x002c0303, + 0x00560004, + 0x00940808, + 0x00d90101, + 0x00130102, + }, + }}; + +int draw_heart_animation(void) +{ + static uint8_t frame_idx = 0; + + // switch row and col order and decrement row to turn image 90 degrees clockwise + for (int col = 0; col < MATRIX_WIDTH; col++) + { + for (int row = MATRIX_HEIGHT - 1; row >= 0; row--) + { + led_matrix.grid_add_pixel(col, row, heart_frames_colormap_11x11[frame_idx][row][col]); + } + } + + // Increase frame index per call until HEART_ANIMATION_FRAMES - 1 for array index. + frame_idx = (frame_idx >= (HEART_ANIMATION_FRAMES - 1)) ? 0 : frame_idx + 1; + return 0; +} \ No newline at end of file diff --git a/src/matrix/led_matrix.cpp b/src/matrix/led_matrix.cpp new file mode 100644 index 0000000..8d06acd --- /dev/null +++ b/src/matrix/led_matrix.cpp @@ -0,0 +1,344 @@ +#include "led_matrix.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(FastLED_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 & UINT8_MAX; + uint8_t g = color_24bit >> 8 & UINT8_MAX; + uint8_t b = color_24bit & UINT8_MAX; + 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 result_R = color1 >> 16 & UINT8_MAX; + uint8_t result_G = color1 >> 8 & UINT8_MAX; + uint8_t result_B = color1 & UINT8_MAX; + result_R = (uint8_t)(result_R + (int16_t)(factor * ((int16_t)(color2 >> 16 & UINT8_MAX) - (int16_t)result_R))); + result_G = (uint8_t)(result_G + (int16_t)(factor * ((int16_t)(color2 >> 8 & UINT8_MAX) - (int16_t)result_G))); + result_B = (uint8_t)(result_B + (int16_t)(factor * ((int16_t)(color2 & UINT8_MAX) - (int16_t)result_B))); + return color_24bit(result_R, result_G, result_B); +} + +/** + * @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_minute_indicators[0] = color; + } + if (pattern >> 1 & 1) + { + _target_minute_indicators[1] = color; + } + if (pattern >> 2 & 1) + { + _target_minute_indicators[2] = color; + } + if (pattern >> 3 & 1) + { + _target_minute_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; + } +} + +/** + * @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_minute_indicators[0] = 0; + _target_minute_indicators[1] = 0; + _target_minute_indicators[2] = 0; + _target_minute_indicators[3] = 0; +} + +uint16_t LEDMatrix::get_fps(void) +{ + return FastLED.getFPS(); +} + +/** + * @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 (uint8_t col = 0; col < MATRIX_WIDTH; col++) + { + for (uint8_t row = 0; row < MATRIX_HEIGHT; row++) + { + // implement momentum as smooth transistion function + filtered_color = interpolate_color_24bit(_current_grid[row][col], _target_grid[row][col], factor); + _neomatrix->drawPixel(col, row, color_24_to_16bit(filtered_color)); + _current_grid[row][col] = filtered_color; + total_current += _calc_estimated_led_current(filtered_color); + } + } + + // loop over all minute indicator leds + for (uint8_t i = 0; i < 4; i++) + { + filtered_color = interpolate_color_24bit(_current_minute_indicators[i], _target_minute_indicators[i], factor); + _neomatrix->drawPixel(MATRIX_WIDTH - (1 + i), MATRIX_HEIGHT, color_24_to_16bit(filtered_color)); + _current_minute_indicators[i] = filtered_color; + total_current += _calc_estimated_led_current(filtered_color); + } + + // Check if totalCurrent reaches _current_limit -> if yes reduce brightness + if (total_current > _current_limit) + { + uint8_t new_brightness = (uint8_t)(_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 & UINT8_MAX; + uint8_t green = color >> 8 & UINT8_MAX; + uint8_t blue = color & UINT8_MAX; + + // 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; +} \ No newline at end of file diff --git a/src/matrix/render_functions.cpp b/src/matrix/render_functions.cpp new file mode 100644 index 0000000..641adef --- /dev/null +++ b/src/matrix/render_functions.cpp @@ -0,0 +1,296 @@ +#include +#include "render_functions.h" +#include "led_matrix.h" +#include "udp_logger.h" + +extern LEDMatrix led_matrix; + +const String clock_chars_as_string = "ESRISTNFUNFVIERTELZEHNZWANZIGHVORPIKACHUNACHHALBMELFUNFMITTERNACHTEINSUWUZWEIDREIFUNVIERSECHSOBACHTSIEBENZWOLFZEHNEUNEUHR"; + +/** + * @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 = clock_chars_as_string.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 += "ZWANZIG NACH "; + } + 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 += "ZWANZIG VOR "; + } + 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 "; + } + + // increment hour when 25 minutes of an hour have passed + if (minutes >= 25) + { + hours++; + } + + // convert hours to 12h format + if (hours > 12) // 24h -> 12h, except 12h + { + hours -= 12; + } + + // show hours + switch (hours) + { + case 0: + { + if (minutes >= 0 && minutes < 5) + { + message += "MITTERNACHT "; + } + else + { + message += "ZWOLF "; + } + break; + } + case 1: + { + message += "EIN"; + message += (minutes > 4) ? "S " : " "; // add "S" if needed + 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; + } + case 12: + { + message += "ZWOLF "; + break; + } + } + if ((minutes < 5) && (hours != 0)) + { + message += "UHR "; + } + + return message; +} + +/** + * @brief Splits a string at given character and return specified element + * + * @param s string to split + * @param parser separating character + * @param index index of the element to return + * @return String + */ +String split(String s, char parser, int index) +{ + String rs = ""; + int parser_cnt = 0; + int r_from_index = 0, r_to_index = -1; + + while (index >= parser_cnt) + { + r_from_index = r_to_index + 1; + r_to_index = s.indexOf(parser, r_from_index); + if (index == parser_cnt) + { + if (r_to_index == 0 || r_to_index == -1) + { + return ""; + } + return s.substring(r_from_index, r_to_index); + } + else + { + parser_cnt++; + } + } + return rs; +} diff --git a/src/wordclock_esp32.cpp b/src/wordclock_esp32.cpp new file mode 100644 index 0000000..ccd3de0 --- /dev/null +++ b/src/wordclock_esp32.cpp @@ -0,0 +1,1094 @@ +/** + * 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_esp32.h" + +#include // https://github.com/adafruit/Adafruit-GFX-Library +#include +#include // from ESP8266 Arduino Core (automatically installed when ESP8266 was installed via Boardmanager) +#include +#include +#include +#include +#include +#include // https://github.com/tzapu/WiFiManager WiFi Configuration Magic + +// own libraries +#include "animation_functions.h" +#include "diagnosis.h" +#include "led_matrix.h" +//#include "littlefs_wrapper.h" +#include "ota_wrapper.h" +#include "pong.h" +#include "render_functions.h" +#include "snake.h" +#include "tetris.h" +#include "udp_logger.h" +#include "wordclock_constants.h" +//#include "time_manager.h" + + +// ---------------------------------------------------------------------------------- +// GLOBAL VARIABLES +// ---------------------------------------------------------------------------------- +UDPLogger logger; // Global UDP logger instance + +CRGB leds[NUM_MATRIX]; // LED array for FastLED +FastLED_NeoMatrix matrix = FastLED_NeoMatrix(leds, MATRIX_WIDTH, (MATRIX_HEIGHT + 1), + NEO_MATRIX_TOP + NEO_MATRIX_LEFT + NEO_MATRIX_ROWS + NEO_MATRIX_ZIGZAG); +LEDMatrix led_matrix = LEDMatrix(&matrix, DEFAULT_BRIGHTNESS, &logger); // FastLED_NeoMatrix wrapper +WebServer 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); + +// State variables +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 ClockState_en current_state = ST_CLOCK; // Stores current state + +// Other variables +static int64_t 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 int64_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}; + +// ---------------------------------------------------------------------------------- +// SETUP +// ---------------------------------------------------------------------------------- +void setup() +{ + // esp_sntp_set_time_sync_notification_cb(&cb); TODO fix me + // 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 + esp_reset_reason_t reset_reason = esp_reset_reason(); + Serial.printf("Reset reason: %u\n", reset_reason); + Serial.println(); + + // Init FastLED + FastLED.addLeds(leds, NUM_MATRIX); + + // 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((uint8_t)0b1111, 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((uint8_t)0b1111, 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_reason != ESP_RST_SW) // only if there was a cold start/hard reset + { + cold_start_setup(); + } + + // set up time manager and get initial time // TODO fix me + // tm_mgr = TimeManager(MY_TZ, NTP_SERVER_URL, check_wifi_status, NTP_MAX_OFFLINE_TIME_S, &logger); + // tm_mgr.init(); + + // configTime(gmtOffset_sec, HOURS_IN_DAY, NTP_SERVER_URL); + + // if (tm_mgr.get_time() == TIME_UPDATE_OK) + // { + // // show the current time for short time in words + // String timeMessage = time_to_string(tm_mgr.hour(), tm_mgr.minute()); + // show_string_on_clock(timeMessage, main_color_clock); + // draw_minute_indicator(tm_mgr.minute(), 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 + random_snake(true, 8, colors_24bit[1], -1); + draw_spiral(true, spiral_direction, MATRIX_WIDTH - 6); + random_tetris(true); + + // Set range limits + limit_value_ranges(); + + // Update brightness + current_brightness = update_brightness(); + + // Send logging data + log_data(); +} + +// ---------------------------------------------------------------------------------- +// LOOP +// ---------------------------------------------------------------------------------- +void loop() +{ + int64_t current_time_us = esp_timer_get_time(); + + // Timestamp variables + static int64_t last_animation_step_us = 0; // timestamp of last animation step + static int64_t last_matrix_update_us = 0; // timestamp of last Matrix update + static int64_t last_time_update_us = 0; // timestamp of last time update + static int64_t last_heartbeat_us = 0; // timestamp of last heartbeat sending + static int64_t last_nightmode_check_us = 0; // timestamp of last nightmode check + static int64_t 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 = esp_timer_get_time(); + } + + 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 = esp_timer_get_time(); + } + + 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 = esp_timer_get_time(); + } + + if ((current_time_us - last_matrix_update_us) >= PERIOD_MATRIX_UPDATE_US) + { + update_matrix(); // update matrix + last_matrix_update_us = esp_timer_get_time(); + } + + if ((current_time_us - last_time_update_us) >= PERIOD_TIME_UPDATE_US) + { + // (void)tm_mgr.get_time(); // NTP time update + + // if (tm_mgr.ntp_sync_timeout()) + // { + // logger.log_string("Trigger restart due to being offline for too long..."); + // delay(100); + // ESP.restart(); + // } + + last_time_update_us = esp_timer_get_time(); + } + + if ((current_time_us - last_nightmode_check_us) >= PERIOD_NIGHTMODE_CHECK_US) + { + check_night_mode(); // check night mode + last_nightmode_check_us = esp_timer_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: " + String(esp_reset_reason()) + "\n"); + 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.tm_state() == TM_NORMAL) // TODO fix me + // { + // (void)show_string_on_clock(time_to_string((uint8_t)tm_mgr.hour(), (uint8_t)tm_mgr.minute()), main_color_clock); + // draw_minute_indicator((uint8_t)tm_mgr.minute(), main_color_clock); + // } + // else if (tm_mgr.ntp_sync_overdue()) // if NTP sync is overdue + // { + // (void)show_string_on_clock(time_to_string((uint8_t)tm_mgr.hour(), (uint8_t)tm_mgr.minute()), main_color_clock); + // draw_minute_indicator((uint8_t)tm_mgr.minute(), colors_24bit[6]); // in blue to indicate a network problem + // } + // else // if no NTP sync has been done, only show 4 blue minute indicators + // { + // // clear matrix + // led_matrix.flush(); + // // Turn on minutes LEDs (blue) + // led_matrix.set_min_indicator((uint8_t)0b1111, colors_24bit[6]); + // led_matrix.draw_on_matrix_instant(); + // } + break; + } + case ST_DICLOCK: // state diclock + { + // if (tm_mgr.ntp_sync_successful()) // TODO fix me + // { + // show_digital_clock((uint8_t)tm_mgr.hour(), (uint8_t)tm_mgr.minute(), main_color_clock); + // } + // else + { + // clear matrix + led_matrix.flush(); + // Turn on minutes LEDs (blue) + led_matrix.set_min_indicator((uint8_t)0b1111, 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 needs to be activated. This only toggles at the exact minute. + int hours; // = tm_mgr.hour(); // TODO fix me + int minutes; // = tm_mgr.minute(); // TODO fix me + + 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 new_state + * + * @param new_state the new state to be changed to + */ +void state_change(ClockState_en new_state) +{ + if (flg_night_mode) + { + set_night_mode(false); // deactivate Nightmode + } + + led_matrix.flush(); // first clear matrix + current_state = new_state; // set new state + on_state_entry((uint8_t)current_state); + logger.log_string("State change to: " + state_names[(uint8_t)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 = esp_timer_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 int64_t button_press_start = 0; // time of push button press start + static int64_t current_system_time = 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 = esp_timer_get_time(); + } + // check falling edge + if (button_pressed == false && last_button_state == true) + { + current_system_time = esp_timer_get_time(); + // button press ended + if ((current_system_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 ((current_system_time - button_press_start) > LONG_PRESS_US) + { + // longpress -> nightmode + logger.log_string("Button press ended - long press"); + set_night_mode(true); + } + else if ((current_system_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((ClockState_en)(((uint8_t)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() +{ + bool send204 = true; // flag to send 204 response + // receive command and handle accordingly +#ifdef SERIAL_DEBUG + for (uint8_t i = 0; i < webserver.args(); i++) + { + Serial.print(webserver.argName(i)); + Serial.print(F(": ")); + Serial.println(webserver.arg(i)); + } +#endif /* SERIAL_DEBUG */ + + 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); + 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); + 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); + 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); + } + } + else if (webserver.argName(0).equals("diag")) + { + String cmd_str = webserver.arg(0); + Diagnosis diag(&logger, &led_matrix); + webserver.send(200, "text/plain", diag.handle_command(cmd_str)); + send204 = false; + } + + if (send204) + { + webserver.send(204, "text/plain", "No Content"); // this page doesn't send back content --> 204 + } +} + +/** + * @brief Handler for GET requests + * + */ +void handle_data_request() +{ +#ifdef SERIAL_DEBUG + // 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)); + } +#endif /* SERIAL_DEBUG */ + 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) // TODO fix me + { + // new_brightness = calculate_dynamic_brightness(brightness_ps->dyn_brightness_min, + // brightness_ps->dyn_brightness_max, + // tm_mgr.hour(), + // tm_mgr.minute(), + // tm_mgr.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); +} diff --git a/src/wrapper/littlefs_wrapper.cpp b/src/wrapper/littlefs_wrapper.cpp new file mode 100644 index 0000000..cd0d18c --- /dev/null +++ b/src/wrapper/littlefs_wrapper.cpp @@ -0,0 +1,230 @@ +// **************************************************************** +// Arduino IDE Tab Esp32 Filesystem Manager spezifisch sortiert Modular +// created: Jens Fleischer, 2023-03-26 +// last mod: Jens Fleischer, 2024-06-03 +// For more information visit: https://fipsok.de +// **************************************************************** +// Hardware: Esp32 +// Software: Esp32 Arduino Core 2.0.6 - 3.0.0 +// Getestet auf: ESP32 NodeMCU-32s +/****************************************************************** + Copyright (c) 2023 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 #include müssen im Haupttab aufgerufen werden +// Die Funktionalität des ESP32 Webservers ist erforderlich. +// "webserver.onNotFound()" darf nicht im Setup des ESP32 Webserver stehen. +// Die Funktion "setupFS();" muss im Setup aufgerufen werden. +/**************************************************************************************/ + +#include +#include "littlefs_wrapper.h" +#include +#ifdef USE_LittleFS +#define SPIFFS LittleFS +#include +#else +#include +#endif +#include +#include +#include + +extern WebServer webserver; + +const char WARNING[] PROGMEM = R"(

LittleFS konnte nicht initialisiert werden!)"; +const char HELPER[] PROGMEM = R"(
+
Lade die fs.html hoch.)"; + +void format_fs() +{ // 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 Speichergrößen + return bytes < 1024 ? static_cast(bytes) + " Byte" : bytes < 1048576 ? static_cast(bytes / 1024.0) + " KB" + : static_cast(bytes / 1048576.0) + " MB"; +} + +bool handle_list() +{ // Senden aller Daten an den Client + File root = LittleFS.open("/"); + using namespace std; + using records = tuple; + list dirList; + while (File f = root.openNextFile()) + { // Ordner und Dateien zur Liste hinzufügen + if (f.isDirectory()) + { + uint8_t ran{0}; + File fold = LittleFS.open(static_cast("/") + f.name()); + while (File f = fold.openNextFile()) + { + ran++; + dirList.emplace_back(fold.name(), f.name(), f.size(), f.getLastWrite()); + } + if (!ran) + dirList.emplace_back(fold.name(), "", 0, 0); + } + else + { + dirList.emplace_back("", f.name(), f.size(), f.getLastWrite()); + } + } + dirList.sort([](const records &f, const records &l) { // Dateien sortieren + if (webserver.arg(0) == "1") + { // nach Größe + return get<2>(f) > get<2>(l); + } + else if (webserver.arg(0) == "2") + { // nach Zeit + return get<3>(f) > get<3>(l); + } + else + { // nach Name + 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)) + "\",\"time\":\"" + get<3>(t) + "\"}"; + } + temp += ",{\"usedBytes\":\"" + format_bytes(LittleFS.usedBytes()) + // Berechnet den verwendeten Speicherplatz + "\",\"totalBytes\":\"" + format_bytes(LittleFS.totalBytes()) + // Zeigt die Größe des Speichers + "\",\"freeBytes\":\"" + (LittleFS.totalBytes() - LittleFS.usedBytes()) + "\"}]"; // Berechnet den freien Speicherplatz + webserver.send(200, "application/json", temp); + return true; +} + +void delete_files(const String &path) +{ + DEBUG_F("delete: %s\n", path.c_str()); + if (!LittleFS.remove("/" + path)) + { + File root = LittleFS.open(path); + while (String filename = root.getNextFileName()) + { + LittleFS.remove(filename); + LittleFS.rmdir(path); + if (filename.length() < 1) + break; + } + } +} + +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 + DEBUG_F("Creating Dir: %s\n", folderName.c_str()); + LittleFS.mkdir("/" + folderName); + } + if (webserver.hasArg("sort")) + return handle_list(); + if (webserver.hasArg("delete")) + { + delete_files(webserver.arg("delete")); + send_response(); + return true; + } + if (!LittleFS.exists("/fs.html")) + webserver.send(200, "text/html", LittleFS.begin(true) ? HELPER : WARNING); // ermöglicht das hochladen der fs.html + if (path.endsWith("/")) + path += "index.html"; + if (path == "/spiffs.html") + send_response(); // Umleitung für den Admin Tab + File f = LittleFS.open(path, "r"); + String eTag = String(f.getLastWrite(), HEX); // Verwendet den Zeitstempel der Dateiänderung, um den ETag zu erstellen. + if (webserver.header("If-None-Match") == eTag) + { + webserver.send(304); + return true; + } + webserver.sendHeader("ETag", eTag); + return LittleFS.exists(path) ? webserver.streamFile(f, StaticRequestHandler::getContentType(path)) : false; +} + +void setup_fs() +{ // Funktionsaufruf "setupFS();" muss im Setup eingebunden werden + LittleFS.begin(true); + webserver.on("/format", format_fs); + webserver.on("/upload", HTTP_POST, send_response, handle_upload); + String path = webserver.urlDecode(webserver.uri()); + webserver.onNotFound([&path]() + { + if (!handle_file(path)) { + webserver.send(404, "text/html", "Page Not Found: " + path); + } }); + const char *headerkeys[] = {"If-None-Match"}; // "If-None-Match" HTTP-Anfrage-Header einfügen + webserver.collectHeaders(headerkeys, static_cast(1)); // für ETag Unterstüzung: vor Core Version 3.x.x. +} + +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 kürzen + upload.filename = upload.filename.substring(upload.filename.length() - 31, upload.filename.length()); + } + DEBUG_F("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) + { + DEBUG_F("handleFileUpload Data: %u\n", upload.currentSize); + fsUploadFile.write(upload.buf, upload.currentSize); + } + else if (upload.status == UPLOAD_FILE_END) + { + DEBUG_F("handleFileUpload Size: %u\n", upload.totalSize); + fsUploadFile.close(); + } +} diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html