├── .github └── workflows │ └── compile_esp8266.yml ├── .gitignore ├── Base64.cpp ├── Base64.h ├── LICENSE ├── LittleFS.ino ├── README.md ├── animationfunctions.ino ├── data ├── fs.html ├── icons │ ├── all_icons.svg │ ├── arrow_left.svg │ ├── arrow_right.svg │ ├── clock.svg │ ├── diclock.svg │ ├── pause.svg │ ├── pingpong.svg │ ├── play.svg │ ├── playpause.svg │ ├── refresh.svg │ ├── settings.svg │ ├── snake.svg │ ├── spiral.svg │ └── tetris.svg ├── index.html └── style.css ├── frontplate ├── frontplate_wordclock2.0_english.svg ├── frontplate_wordclock2.0_french.svg ├── frontplate_wordclock2.0_german.svg ├── frontplate_wordclock2.0_german_20x20cm.svg └── frontplate_wordclock2.0_italian.svg ├── grid └── grid_wordclock2.0_20x20cm.stl ├── ledmatrix.cpp ├── ledmatrix.h ├── multicastUDP_receiver.py ├── ntp_client_plus.cpp ├── ntp_client_plus.h ├── otafunctions.ino ├── own_font.h ├── pong.cpp ├── pong.h ├── secrets_example.h ├── snake.cpp ├── snake.h ├── tetris.cpp ├── tetris.h ├── timezonefunctions.ino ├── udplogger.cpp ├── udplogger.h ├── wordclock_esp8266.ino ├── wordclockfunctions.ino ├── wordclockfunctions.ino_english ├── wordclockfunctions.ino_french ├── wordclockfunctions.ino_italian ├── wordclockfunctions.ino_javanese └── wordclockfunctions.ino_swiss /.github/workflows/compile_esp8266.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Compile ESP8266 Sketch 3 | 4 | on: 5 | push: 6 | pull_request: 7 | 8 | jobs: 9 | compile: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout Repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Install Arduino CLI 17 | run: | 18 | curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh 19 | echo "$(pwd)/bin" >> $GITHUB_PATH 20 | 21 | - name: Configure Arduino CLI 22 | run: | 23 | cat < .cli-config.yml 24 | board_manager: 25 | additional_urls: 26 | - http://arduino.esp8266.com/stable/package_esp8266com_index.json 27 | EOF 28 | arduino-cli config init 29 | arduino-cli core update-index 30 | arduino-cli core install esp8266:esp8266 --config-file .cli-config.yml 31 | 32 | - name: Install Required Libraries 33 | run: | 34 | arduino-cli lib install "Adafruit GFX Library" 35 | arduino-cli lib install "Adafruit NeoMatrix" 36 | arduino-cli lib install "Adafruit NeoPixel" 37 | arduino-cli lib install "WiFiManager" 38 | arduino-cli lib install "Adafruit BusIO" 39 | 40 | - name: Compile Sketch 41 | run: | 42 | mv secrets_example.h secrets.h 43 | arduino-cli compile -v --fqbn esp8266:esp8266:nodemcuv2 wordclock_esp8266.ino 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | secrets.h 2 | /.vscode 3 | /build 4 | log*.txt -------------------------------------------------------------------------------- /Base64.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Arturo Guadalupi. All right reserved. 3 | 4 | This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. 5 | 6 | This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. 7 | */ 8 | 9 | #include "Base64.h" 10 | #include 11 | #if (defined(__AVR__)) 12 | #include 13 | #else 14 | #include 15 | #endif 16 | 17 | const char PROGMEM _Base64AlphabetTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 18 | "abcdefghijklmnopqrstuvwxyz" 19 | "0123456789+/"; 20 | 21 | int Base64Class::encode(char *output, char *input, int inputLength) { 22 | int i = 0, j = 0; 23 | int encodedLength = 0; 24 | unsigned char A3[3]; 25 | unsigned char A4[4]; 26 | 27 | while(inputLength--) { 28 | A3[i++] = *(input++); 29 | if(i == 3) { 30 | fromA3ToA4(A4, A3); 31 | 32 | for(i = 0; i < 4; i++) { 33 | output[encodedLength++] = pgm_read_byte(&_Base64AlphabetTable[A4[i]]); 34 | } 35 | 36 | i = 0; 37 | } 38 | } 39 | 40 | if(i) { 41 | for(j = i; j < 3; j++) { 42 | A3[j] = '\0'; 43 | } 44 | 45 | fromA3ToA4(A4, A3); 46 | 47 | for(j = 0; j < i + 1; j++) { 48 | output[encodedLength++] = pgm_read_byte(&_Base64AlphabetTable[A4[j]]); 49 | } 50 | 51 | while((i++ < 3)) { 52 | output[encodedLength++] = '='; 53 | } 54 | } 55 | output[encodedLength] = '\0'; 56 | return encodedLength; 57 | } 58 | 59 | int Base64Class::decode(char * output, char * input, int inputLength) { 60 | int i = 0, j = 0; 61 | int decodedLength = 0; 62 | unsigned char A3[3]; 63 | unsigned char A4[4]; 64 | 65 | 66 | while (inputLength--) { 67 | if(*input == '=') { 68 | break; 69 | } 70 | 71 | A4[i++] = *(input++); 72 | if (i == 4) { 73 | for (i = 0; i <4; i++) { 74 | A4[i] = lookupTable(A4[i]); 75 | } 76 | 77 | fromA4ToA3(A3,A4); 78 | 79 | for (i = 0; i < 3; i++) { 80 | output[decodedLength++] = A3[i]; 81 | } 82 | i = 0; 83 | } 84 | } 85 | 86 | if (i) { 87 | for (j = i; j < 4; j++) { 88 | A4[j] = '\0'; 89 | } 90 | 91 | for (j = 0; j <4; j++) { 92 | A4[j] = lookupTable(A4[j]); 93 | } 94 | 95 | fromA4ToA3(A3,A4); 96 | 97 | for (j = 0; j < i - 1; j++) { 98 | output[decodedLength++] = A3[j]; 99 | } 100 | } 101 | output[decodedLength] = '\0'; 102 | return decodedLength; 103 | } 104 | 105 | int Base64Class::encodedLength(int plainLength) { 106 | int n = plainLength; 107 | return (n + 2 - ((n + 2) % 3)) / 3 * 4; 108 | } 109 | 110 | int Base64Class::decodedLength(char * input, int inputLength) { 111 | int i = 0; 112 | int numEq = 0; 113 | for(i = inputLength - 1; input[i] == '='; i--) { 114 | numEq++; 115 | } 116 | 117 | return ((6 * inputLength) / 8) - numEq; 118 | } 119 | 120 | //Private utility functions 121 | inline void Base64Class::fromA3ToA4(unsigned char * A4, unsigned char * A3) { 122 | A4[0] = (A3[0] & 0xfc) >> 2; 123 | A4[1] = ((A3[0] & 0x03) << 4) + ((A3[1] & 0xf0) >> 4); 124 | A4[2] = ((A3[1] & 0x0f) << 2) + ((A3[2] & 0xc0) >> 6); 125 | A4[3] = (A3[2] & 0x3f); 126 | } 127 | 128 | inline void Base64Class::fromA4ToA3(unsigned char * A3, unsigned char * A4) { 129 | A3[0] = (A4[0] << 2) + ((A4[1] & 0x30) >> 4); 130 | A3[1] = ((A4[1] & 0xf) << 4) + ((A4[2] & 0x3c) >> 2); 131 | A3[2] = ((A4[2] & 0x3) << 6) + A4[3]; 132 | } 133 | 134 | inline unsigned char Base64Class::lookupTable(char c) { 135 | if(c >='A' && c <='Z') return c - 'A'; 136 | if(c >='a' && c <='z') return c - 71; 137 | if(c >='0' && c <='9') return c + 4; 138 | if(c == '+') return 62; 139 | if(c == '/') return 63; 140 | return -1; 141 | } 142 | 143 | Base64Class Base64; 144 | -------------------------------------------------------------------------------- /Base64.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Arturo Guadalupi. All right reserved. 3 | 4 | This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. 5 | 6 | This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. 7 | */ 8 | 9 | #ifndef _BASE64_H 10 | #define _BASE64_H 11 | 12 | class Base64Class{ 13 | public: 14 | int encode(char *output, char *input, int inputLength); 15 | int decode(char * output, char * input, int inputLength); 16 | int encodedLength(int plainLength); 17 | int decodedLength(char * input, int inputLength); 18 | 19 | private: 20 | inline void fromA3ToA4(unsigned char * A4, unsigned char * A3); 21 | inline void fromA4ToA3(unsigned char * A3, unsigned char * A4); 22 | inline unsigned char lookupTable(char c); 23 | }; 24 | extern Base64Class Base64; 25 | 26 | #endif // _BASE64_H 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Techniccontroller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LittleFS.ino: -------------------------------------------------------------------------------- 1 | 2 | // **************************************************************** 3 | // Sketch Esp8266 Filesystem Manager spezifisch sortiert Modular(Tab) 4 | // created: Jens Fleischer, 2020-06-08 5 | // last mod: Jens Fleischer, 2020-12-19 6 | // For more information visit: https://fipsok.de 7 | // **************************************************************** 8 | // Hardware: Esp8266 9 | // Software: Esp8266 Arduino Core 2.7.0 - 3.0.2 10 | // Geprüft: von 1MB bis 2MB Flash 11 | // Getestet auf: Nodemcu 12 | /****************************************************************** 13 | Copyright (c) 2020 Jens Fleischer. All rights reserved. 14 | 15 | This file is free software; you can redistribute it and/or 16 | modify it under the terms of the GNU Lesser General Public 17 | License as published by the Free Software Foundation; either 18 | version 2.1 of the License, or (at your option) any later version. 19 | This file is distributed in the hope that it will be useful, 20 | but WITHOUT ANY WARRANTY; without even the implied warranty of 21 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 22 | Lesser General Public License for more details. 23 | *******************************************************************/ 24 | // Diese Version von LittleFS sollte als Tab eingebunden werden. 25 | // #include #include müssen im Haupttab aufgerufen werden 26 | // Die Funktionalität des ESP8266 Webservers ist erforderlich. 27 | // "server.onNotFound()" darf nicht im Setup des ESP8266 Webserver stehen. 28 | // Die Funktion "setupFS();" muss im Setup aufgerufen werden. 29 | /**************************************************************************************/ 30 | 31 | #include 32 | #include 33 | 34 | const char WARNING[] PROGMEM = R"(

Der Sketch wurde mit "FS:none" kompilliert!)"; 35 | const char HELPER[] PROGMEM = R"(
36 |
Lade die fs.html hoch.)"; 37 | 38 | void setupFS() { // Funktionsaufruf "setupFS();" muss im Setup eingebunden werden 39 | LittleFS.begin(); 40 | server.on("/format", formatFS); 41 | server.on("/upload", HTTP_POST, sendResponce, handleUpload); 42 | server.onNotFound([]() { 43 | if (!handleFile(server.urlDecode(server.uri()))) 44 | server.send(404, "text/plain", "FileNotFound"); 45 | }); 46 | } 47 | 48 | bool handleList() { // Senden aller Daten an den Client 49 | FSInfo fs_info; LittleFS.info(fs_info); // Füllt FSInfo Struktur mit Informationen über das Dateisystem 50 | Dir dir = LittleFS.openDir("/"); 51 | using namespace std; 52 | using records = tuple; 53 | list dirList; 54 | while (dir.next()) { // Ordner und Dateien zur Liste hinzufügen 55 | if (dir.isDirectory()) { 56 | uint8_t ran {0}; 57 | Dir fold = LittleFS.openDir(dir.fileName()); 58 | while (fold.next()) { 59 | ran++; 60 | dirList.emplace_back(dir.fileName(), fold.fileName(), fold.fileSize()); 61 | } 62 | if (!ran) dirList.emplace_back(dir.fileName(), "", 0); 63 | } 64 | else { 65 | dirList.emplace_back("", dir.fileName(), dir.fileSize()); 66 | } 67 | } 68 | dirList.sort([](const records & f, const records & l) { // Dateien sortieren 69 | if (server.arg(0) == "1") { 70 | return get<2>(f) > get<2>(l); 71 | } else { 72 | for (uint8_t i = 0; i < 31; i++) { 73 | if (tolower(get<1>(f)[i]) < tolower(get<1>(l)[i])) return true; 74 | else if (tolower(get<1>(f)[i]) > tolower(get<1>(l)[i])) return false; 75 | } 76 | return false; 77 | } 78 | }); 79 | dirList.sort([](const records & f, const records & l) { // Ordner sortieren 80 | if (get<0>(f)[0] != 0x00 || get<0>(l)[0] != 0x00) { 81 | for (uint8_t i = 0; i < 31; i++) { 82 | if (tolower(get<0>(f)[i]) < tolower(get<0>(l)[i])) return true; 83 | else if (tolower(get<0>(f)[i]) > tolower(get<0>(l)[i])) return false; 84 | } 85 | } 86 | return false; 87 | }); 88 | String temp = "["; 89 | for (auto& t : dirList) { 90 | if (temp != "[") temp += ','; 91 | temp += "{\"folder\":\"" + get<0>(t) + "\",\"name\":\"" + get<1>(t) + "\",\"size\":\"" + formatBytes(get<2>(t)) + "\"}"; 92 | } 93 | temp += ",{\"usedBytes\":\"" + formatBytes(fs_info.usedBytes) + // Berechnet den verwendeten Speicherplatz 94 | "\",\"totalBytes\":\"" + formatBytes(fs_info.totalBytes) + // Zeigt die Größe des Speichers 95 | "\",\"freeBytes\":\"" + (fs_info.totalBytes - fs_info.usedBytes) + "\"}]"; // Berechnet den freien Speicherplatz 96 | server.send(200, "application/json", temp); 97 | return true; 98 | } 99 | 100 | void deleteRecursive(const String &path) { 101 | if (LittleFS.remove(path)) { 102 | LittleFS.open(path.substring(0, path.lastIndexOf('/')) + "/", "w"); 103 | return; 104 | } 105 | Dir dir = LittleFS.openDir(path); 106 | while (dir.next()) { 107 | deleteRecursive(path + '/' + dir.fileName()); 108 | } 109 | LittleFS.rmdir(path); 110 | } 111 | 112 | bool handleFile(String &&path) { 113 | if (server.hasArg("new")) { 114 | String folderName {server.arg("new")}; 115 | for (auto& c : {34, 37, 38, 47, 58, 59, 92}) for (auto& e : folderName) if (e == c) e = 95; // Ersetzen der nicht erlaubten Zeichen 116 | LittleFS.mkdir(folderName); 117 | } 118 | if (server.hasArg("sort")) return handleList(); 119 | if (server.hasArg("delete")) { 120 | deleteRecursive(server.arg("delete")); 121 | sendResponce(); 122 | return true; 123 | } 124 | if (!LittleFS.exists("fs.html")) server.send(200, "text/html", LittleFS.begin() ? HELPER : WARNING); // ermöglicht das hochladen der fs.html 125 | if (path.endsWith("/")) path += "index.html"; 126 | if (path == "/spiffs.html") sendResponce(); // Vorrübergehend für den Admin Tab 127 | return LittleFS.exists(path) ? ({File f = LittleFS.open(path, "r"); server.streamFile(f, mime::getContentType(path)); f.close(); true;}) : false; 128 | } 129 | 130 | void handleUpload() { // Dateien ins Filesystem schreiben 131 | static File fsUploadFile; 132 | HTTPUpload& upload = server.upload(); 133 | if (upload.status == UPLOAD_FILE_START) { 134 | if (upload.filename.length() > 31) { // Dateinamen kürzen 135 | upload.filename = upload.filename.substring(upload.filename.length() - 31, upload.filename.length()); 136 | } 137 | printf(PSTR("handleFileUpload Name: /%s\n"), upload.filename.c_str()); 138 | fsUploadFile = LittleFS.open(server.arg(0) + "/" + server.urlDecode(upload.filename), "w"); 139 | } else if (upload.status == UPLOAD_FILE_WRITE) { 140 | printf(PSTR("handleFileUpload Data: %u\n"), upload.currentSize); 141 | fsUploadFile.write(upload.buf, upload.currentSize); 142 | } else if (upload.status == UPLOAD_FILE_END) { 143 | printf(PSTR("handleFileUpload Size: %u\n"), upload.totalSize); 144 | fsUploadFile.close(); 145 | } 146 | } 147 | 148 | void formatFS() { // Formatiert das Filesystem 149 | LittleFS.format(); 150 | sendResponce(); 151 | } 152 | 153 | void sendResponce() { 154 | server.sendHeader("Location", "fs.html"); 155 | server.send(303, "message/http"); 156 | } 157 | 158 | const String formatBytes(size_t const& bytes) { // lesbare Anzeige der Speichergrößen 159 | return bytes < 1024 ? static_cast(bytes) + " Byte" : bytes < 1048576 ? static_cast(bytes / 1024.0) + " KB" : static_cast(bytes / 1048576.0) + " MB"; 160 | } 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wordclock 2.0 2 | ![compile esp8266 workflow](https://github.com/techniccontroller/wordclock_esp8266/actions/workflows/compile_esp8266.yml/badge.svg?branch=main) 3 | 4 | Wordclock 2.0 with ESP8266 and NTP time 5 | 6 | More details on my website: https://techniccontroller.com/word-clock-with-wifi-and-neopixel/ 7 | 8 | 9 | **Languages** 10 | 11 | The Wordclock is available in **German**, **English**, **Italian**, **French**, **Swiss German**, and **Javanese** language. By default the language is German. 12 | To use other languages like English or Italian please replace the file *wordclockfunctions.ino* with *wordclockfunctions.ino_english* or *wordclockfunctions.ino_italian*. 13 | The code compiles only with one file named *wordclockfunctions.ino*. So please rename the file you want to use to *wordclockfunctions.ino* and replace the existing file. 14 | 15 | Thank you to everyone who provided feedback on adding new languages and testing their accuracy — your efforts have been invaluable in making this project truly inclusive and reliable! 16 | 17 | **Special Branches** 18 | 19 | We've got some interesting branches in this repo inspired by user feedback. These branches explore unique features and experimental ideas. Some will stay updated with the main branch's features. 20 | 21 | - [**hour_animation**](https://github.com/techniccontroller/wordclock_esp8266/tree/hour_animation): This branch replaces the spiral animation with some custom pattern animation defined as x/y coordinate pattern including custom color for each letter. Also, this animation is show ones per hour. 22 | ![compile esp8266 workflow](https://github.com/techniccontroller/wordclock_esp8266/actions/workflows/compile_esp8266.yml/badge.svg?branch=hour_animation) 23 | - [**mode_seconds**](https://github.com/techniccontroller/wordclock_esp8266/tree/mode_seconds): This branch adds one additional mode to show the seconds as numbers on the clock. Thanks to [@Bram](https://github.com/BramWerbrouck) 24 | ![compile esp8266 workflow](https://github.com/techniccontroller/wordclock_esp8266/actions/workflows/compile_esp8266.yml/badge.svg?branch=mode_seconds) 25 | - [**rgbw_leds**](https://github.com/techniccontroller/wordclock_esp8266/tree/rgbw_leds): This branch uses RGBW LEDs instead of RGB LEDs. 26 | ![compile esp8266 workflow](https://github.com/techniccontroller/wordclock_esp8266/actions/workflows/compile_esp8266.yml/badge.svg?branch=rgbw_leds) 27 | - [**static_background_pattern**](https://github.com/techniccontroller/wordclock_esp8266/tree/static_background_pattern): This branch allows to light up specific letters always during clock mode. E.G., to display some special words in another color. 28 | ![compile esp8266 workflow](https://github.com/techniccontroller/wordclock_esp8266/actions/workflows/compile_esp8266.yml/badge.svg?branch=static_background_pattern) 29 | 30 | 31 | 32 | ## Features 33 | - 6 modes (Clock, Digital Clock, SPIRAL animation, TETRIS, SNAKE, PONG) 34 | - time update via NTP server 35 | - automatic summer/wintertime change 36 | - automatic timezone selection 37 | - easy WIFI setup with WifiManager 38 | - configurable color 39 | - configurable night mode (start and end time) 40 | - configurable brightness 41 | - automatic mode change 42 | - webserver interface for configuration and control 43 | - physical button to change mode or enable night mode without webserver 44 | - automatic current limiting of LEDs 45 | - dynamic color shift mode 46 | 47 | ## Pictures of clock 48 | ![modes_images2](https://user-images.githubusercontent.com/36072504/156947689-dd90874d-a887-4254-bede-4947152d85c1.png) 49 | 50 | ## Screenshots of webserver UI 51 | ![screenshots_UI](https://user-images.githubusercontent.com/36072504/158478447-d828e460-d4eb-489e-981e-216e08d4b129.png) 52 | 53 | ## Quickstart 54 | 55 | 1. Clone the project into the sketch folder of the Arduino IDE, 56 | 2. Rename the file "example_secrets.h" to "secrets.h". You don't need to change anything in the file if you want to use the normal WiFi setup with WiFiManager (see section "Remark about the WiFi setup"). 57 | 3. Install the additional libraries and upload the program to the ESP8266 as usual (See section [*Upload program to ESP8266*](https://github.com/techniccontroller/wordclock_esp8266/blob/main/README.md#upload-program-to-esp8266-with-arduino-ide) below). 58 | 4. The implemented WiFiManager helps you to set up a WiFi connection with your home WiFi -> on the first startup it will create a WiFi access point named "WordclockAP". Connect your phone to this access point and follow the steps which will be shown to you. 59 | 5. After a successful WiFi setup, open the browser and enter the IP address of your ESP8266 to access the interface of the webserver. 60 | 6. Here you can upload all files located in the folder "data". Please make sure all icons stay in the folder "icons" also on the webserver. 61 | - Open **http://\/fs.html** in a browser 62 | - Upload **fs.html** 63 | - Upload **style.css** 64 | - Upload **index.html** 65 | - Create a new folder **icons** 66 | - Upload all icons into this new folder **icons** 67 | 68 | 69 | 70 | 71 | ## Install needed Libraries 72 | 73 | Please download all these libraries as ZIP from GitHub, and extract them in the *libraries* folder of your Sketchbook location (see **File -> Preferences**): 74 | 75 | - https://github.com/adafruit/Adafruit-GFX-Library 76 | - https://github.com/adafruit/Adafruit_NeoMatrix 77 | - https://github.com/adafruit/Adafruit_NeoPixel 78 | - https://github.com/tzapu/WiFiManager 79 | - https://github.com/adafruit/Adafruit_BusIO 80 | 81 | You can als install these libraries via the library manager in the Arduino IDE. 82 | 83 | The folder structure should look like this: 84 | 85 | ``` 86 | MySketchbookLocation 87 | │ 88 | └───libraries 89 | │ └───Adafruit-GFX-Library 90 | │ └───Adafruit_NeoMatrix 91 | │ └───Adafruit_NeoPixel 92 | │ └───WiFiManager 93 | │ └───Adafruit_BusIO 94 | │ 95 | └───wordclock_esp8266 96 | │ wordclock_esp8266.ino 97 | │ (...) 98 | | 99 | └───data 100 | │ index.html 101 | | (...) 102 | | 103 | └───icons 104 | ``` 105 | 106 | 107 | ## Upload program to ESP8266 with Arduino IDE 108 | 109 | #### STEP1: Installation of Arduino IDE 110 | First, the latest version of the Arduino IDE needs to be downloaded and installed from [here](https://www.arduino.cc/en/software). 111 | 112 | #### STEP2: Installation of ESP8266 Arduino Core 113 | To program the ESP8266 with the Arduino IDE, you need to install the board information first in Arduino IDE. To do that follow the following instructions: 114 | 115 | - Start Arduino and open the File -> Preferences window. 116 | 117 | - Enter http://arduino.esp8266.com/stable/package_esp8266com_index.json into the Additional Board Manager URLs field. You can add multiple URLs, separating them with commas. 118 | ![image](https://user-images.githubusercontent.com/36072504/169649790-1b85660e-8c7d-4dfe-a63a-5dfd9862a5de.png) 119 | 120 | - Open Boards Manager from Tools > Board menu and search for "esp8266". 121 | 122 | - Click the install button. 123 | 124 | - Don’t forget to select your ESP8266 board from Tools > Board menu after installation (e.g NodeMCU 1.0) 125 | ![image](https://user-images.githubusercontent.com/36072504/169649801-898c4819-9145-45c5-b65b-52f2689ab646.png) 126 | 127 | #### STEP3: Upload a program to ESP8266 128 | 129 | - Open wordclock_esp8266.ino in Arduino IDE 130 | - Connect ESP8266 board with Computer 131 | - Select right serial Port in Tools -> Port 132 | - Click on the upload button in the Arduino IDE to upload the program to the ESP8266 Module. 133 | ![image](https://user-images.githubusercontent.com/36072504/169649810-1fda75c2-5f4d-4d71-98fe-30985d82f7f5.png) 134 | 135 | 136 | ## Remark about the WiFi setup 137 | 138 | Regarding the WiFi setting, I have actually implemented two variants: 139 | 1. By default the WifiManager is activated. That is, the word clock makes the first time its own WiFi (should be called "WordclockAP"). There you connect from a cell phone to `192.168.4.1`* and you can perform the configuration of the WiFi settings conveniently as with a SmartHome devices (Very elegant 😊) 140 | 2. Another (traditional) variant is to define the wifi credentials in the code (in secrets.h). 141 | - For this you have to comment out lines 230 to 251 in the code of the file *wordclock_esp8266.ino* (add /\* before and \*/ after) 142 | - and comment out lines 257 to 305 (remove /\* and \*/) 143 | (* default IP provided by the WifiMAnager library.) 144 | 145 | ## Resetting the WiFi configuration 146 | 147 | You can clear the stored WiFi credentials and restart the WiFi setup described above with these steps: 148 | 1. Open the settings panel in the web UI. 149 | 2. Enable 'Reset WiFi' slider. 150 | 3. Save settings. 151 | 4. LED test should be performed. 152 | 5. Disconnect and reconnect the power. WiFi credentials were removed. The setup should be restarted. 153 | Resetting the wifi credentials does not delete uploaded files. 154 | 155 | ## Remark about Logging 156 | 157 | The wordclock sends continuous log messages to the serial port and via multicast UDP. If you want to see these messages, you have to 158 | 159 | - open the serial monitor in the Arduino IDE (Tools -> Serial Monitor). The serial monitor must be set to 115200 baud. 160 | 161 | OR 162 | 163 | - run the following steps for the multicast UDP logging: 164 | 165 | 1. Starting situation: wordclock is connected to WLAN, a computer with installed Python (https://www.python.org/downloads/) is in the same local area network (WLAN or LAN doesn't matter). 166 | 3. Open the file **multicastUDP_receiver.py** in a text editor and in line 81 enter the IP address of the computer (not the wordclock!). 167 | ```python 168 | # ip address of network interface 169 | MCAST_IF_IP = '192.168.0.7' 170 | ``` 171 | 4. Execute the script with following command: 172 | 173 | ```bash 174 | python multicastUDP_receiver.py 175 | ``` 176 | 177 | 5. Now you should see the log messages of the word clock (every 5 seconds a heartbeat message and the currently displayed time). 178 | If this is not the case, there could be a problem with the network settings of the computer, then recording is unfortunately not possible. 179 | 180 | 6. If special events (failed NTP update, reboot) occur, a section of the log is saved in a file called *log.txt*. 181 | In principle, the events are not critical and will occur from time to time, but should not be too frequent. 182 | -------------------------------------------------------------------------------- /animationfunctions.ino: -------------------------------------------------------------------------------- 1 | const int8_t dx[] = {1, -1, 0, 0}; 2 | const int8_t dy[] = {0, 0, -1, 1}; 3 | 4 | /** 5 | * @brief Function to draw a spiral step (from center) 6 | * 7 | * @param init marks if call is the initial step of the spiral 8 | * @param empty marks if the spiral should 'draw' empty leds 9 | * @param size the size of the spiral in leds 10 | * @return int - 1 if end is reached, else 0 11 | */ 12 | int spiral(bool init, bool empty, uint8_t size){ 13 | static direction dir1; // current direction 14 | static int x; 15 | static int y; 16 | static int counter1; 17 | static int countStep; 18 | static int countEdge; 19 | static int countCorner; 20 | static bool breiter ; 21 | static int randNum; 22 | if(init){ 23 | logger.logString("Init Spiral with empty=" + String(empty)); 24 | dir1 = down; // current direction 25 | x = WIDTH/2; 26 | y = WIDTH/2; 27 | if(!empty)ledmatrix.gridFlush(); 28 | counter1 = 0; 29 | countStep = 0; 30 | countEdge = 1; 31 | countCorner = 0; 32 | breiter = true; 33 | randNum = random(255); 34 | } 35 | 36 | if (countStep == size*size){ 37 | // End reached return 1 38 | return 1; 39 | } 40 | else{ 41 | // calc color from colorwheel 42 | uint32_t color = LEDMatrix::Wheel((randNum +countStep*6)%255); 43 | // if draw mode is empty, set color to zero 44 | if(empty){ 45 | color = 0; 46 | } 47 | ledmatrix.gridAddPixel(x, y, color); 48 | if(countCorner == 2 && breiter){ 49 | countEdge +=1; 50 | breiter = false; 51 | } 52 | if(counter1 >= countEdge){ 53 | dir1 = nextDir(dir1, LEFT); 54 | counter1 = 0; 55 | countCorner++; 56 | } 57 | if(countCorner >= 4){ 58 | countCorner = 0; 59 | countEdge += 1; 60 | breiter = true; 61 | } 62 | 63 | x += dx[dir1]; 64 | y += dy[dir1]; 65 | //logger.logString("x: " + String(x) + ", y: " + String(y) + "c: " + String(color) + "\n"); 66 | counter1++; 67 | countStep++; 68 | } 69 | return 0; 70 | } 71 | 72 | /** 73 | * @brief Run random snake animation 74 | * 75 | * @param init marks if call is the initial step of the animation 76 | * @param len length of the snake 77 | * @param color color of the snake 78 | * @param numSteps number of animation steps 79 | * @return int - 1 when animation is finished, else 0 80 | */ 81 | int randomsnake(bool init, const uint8_t len, const uint32_t color, int numSteps){ 82 | static direction dir1; 83 | static int snake1[2][10]; 84 | static int randomy; 85 | static int randomx; 86 | static int e; 87 | static int countStep; 88 | if(init){ 89 | dir1 = down; // current direction 90 | for(int i = 0; i < len; i++){ 91 | snake1[0][i] = 3; 92 | snake1[1][i] = i; 93 | } 94 | 95 | randomy = random(1,8); // Random variable for y-direction 96 | randomx = random(1,4); // Random variable for x-direction 97 | e = LEFT; // next turn 98 | countStep = 0; 99 | } 100 | if (countStep == numSteps){ 101 | // End reached return 1 102 | return 1; 103 | } 104 | else{ 105 | // move one step forward 106 | for(int i = 0; i < len; i++){ 107 | if(i < len-1){ 108 | snake1[0][i] = snake1[0][i+1]; 109 | snake1[1][i] = snake1[1][i+1]; 110 | }else{ 111 | snake1[0][i] = snake1[0][i]+dx[dir1]; 112 | snake1[1][i] = snake1[1][i]+dy[dir1]; 113 | } 114 | } 115 | // collision with wall? 116 | if( (dir1 == down && snake1[1][len-1] >= HEIGHT-1) || 117 | (dir1 == up && snake1[1][len-1] <= 0) || 118 | (dir1 == right && snake1[0][len-1] >= WIDTH-1) || 119 | (dir1 == left && snake1[0][len-1] <= 0)){ 120 | dir1 = nextDir(dir1, e); 121 | } 122 | // Random branching at the side edges 123 | else if((dir1 == up && snake1[1][len-1] == randomy && snake1[0][len-1] >= WIDTH-1) || (dir1 == down && snake1[1][len-1] == randomy && snake1[0][len-1] <= 0)){ 124 | dir1 = nextDir(dir1, LEFT); 125 | e = (e+2)%2+1; 126 | } 127 | else if((dir1 == down && snake1[1][len-1] == randomy && snake1[0][len-1] >= WIDTH-1) || (dir1 == up && snake1[1][len-1] == randomy && snake1[0][len-1] <= 0)){ 128 | dir1 = nextDir(dir1, RIGHT); 129 | e = (e+2)%2+1; 130 | } 131 | else if((dir1 == left && snake1[0][len-1] == randomx && snake1[1][len-1] <= 0) || (dir1 == right && snake1[0][len-1] == randomx && snake1[1][len-1] >= HEIGHT-1)){ 132 | dir1 = nextDir(dir1, LEFT); 133 | e = (e+2)%2+1; 134 | } 135 | else if((dir1 == right && snake1[0][len-1] == randomx && snake1[1][len-1] <= 0) || (dir1 == left && snake1[0][len-1] == randomx && snake1[1][len-1] >= HEIGHT-1)){ 136 | dir1 = nextDir(dir1, RIGHT); 137 | e = (e+2)%2+1; 138 | } 139 | 140 | 141 | for(int i = 0; i < len; i++){ 142 | // draw the snake 143 | ledmatrix.gridAddPixel(snake1[0][i], snake1[1][i], color); 144 | } 145 | 146 | // calc new random variables after every 20 steps 147 | if(countStep%20== 0){ 148 | randomy = random(1,8); 149 | randomx = random(1,4); 150 | } 151 | countStep++; 152 | } 153 | return 0; 154 | } 155 | 156 | /** 157 | * @brief Calc the next direction for led movement (snake and spiral) 158 | * 159 | * @param dir direction of the current led movement 160 | * @param d action to be executed 161 | * @return direction - next direction 162 | */ 163 | direction nextDir(direction dir, int d){ 164 | // d = 0 -> continue straight on 165 | // d = 1 -> turn LEFT 166 | // d = 2 -> turn RIGHT 167 | direction selection[3]; 168 | switch(dir){ 169 | case right: 170 | selection[0] = right; 171 | selection[1] = up; 172 | selection[2] = down; 173 | break; 174 | case left: 175 | selection[0] = left; 176 | selection[1] = down; 177 | selection[2] = up; 178 | break; 179 | case up: 180 | selection[0] = up; 181 | selection[1] = left; 182 | selection[2] = right; 183 | break; 184 | case down: 185 | selection[0] = down; 186 | selection[1] = right; 187 | selection[2] = left; 188 | break; 189 | } 190 | direction next = selection[d]; 191 | return next; 192 | } 193 | 194 | /** 195 | * @brief Show the time as digits on the wordclock 196 | * 197 | * @param hours hours of time to display 198 | * @param minutes minutes of time to display 199 | * @param color color to display (24bit) 200 | */ 201 | void showDigitalClock(uint8_t hours, uint8_t minutes, uint32_t color){ 202 | ledmatrix.gridFlush(); 203 | uint8_t fstDigitH = hours/10; 204 | uint8_t sndDigitH = hours%10; 205 | uint8_t fstDigitM = minutes/10; 206 | uint8_t sndDigitM = minutes%10; 207 | ledmatrix.printNumber(2, 0, fstDigitH, color); 208 | ledmatrix.printNumber(6, 0, sndDigitH, color); 209 | ledmatrix.printNumber(2, 6, fstDigitM, color); 210 | ledmatrix.printNumber(6, 6, sndDigitM, color); 211 | } 212 | 213 | /** 214 | * @brief Run random tetris animation 215 | * 216 | * @param init marks if call is the initial step of the animation 217 | * @return int - 1 when animation is finished, else 0 218 | */ 219 | int randomtetris(bool init){ 220 | // total number of blocks which can be displayed 221 | const static uint8_t numBlocks = 30; 222 | // all different block shapes 223 | const static bool blockshapes[9][3][3]={{ {0,0,0}, 224 | {0,0,0}, 225 | {0,0,0}}, 226 | { {1,0,0}, 227 | {1,0,0}, 228 | {1,0,0}}, 229 | { {0,0,0}, 230 | {1,0,0}, 231 | {1,0,0}}, 232 | { {0,0,0}, 233 | {1,1,0}, 234 | {1,0,0}}, 235 | { {0,0,0}, 236 | {0,0,0}, 237 | {1,1,0}}, 238 | { {0,0,0}, 239 | {1,1,0}, 240 | {1,1,0}}, 241 | { {0,0,0}, 242 | {0,0,0}, 243 | {1,1,1}}, 244 | { {0,0,0}, 245 | {1,1,1}, 246 | {1,0,0}}, 247 | { {0,0,0}, 248 | {0,0,1}, 249 | {1,1,1}}}; 250 | // local game screen buffer 251 | static uint8_t screen[HEIGHT+3][WIDTH]; 252 | // current number of blocks on the screen 253 | static int counterID; 254 | // indicate if the game was lost 255 | static bool gameover = false; 256 | 257 | 258 | if(init || gameover){ 259 | logger.logString("Init Tetris: init=" + String(init) + ", gameover=" + String(gameover)); 260 | // clear local game screen 261 | for(int h = 0; h < HEIGHT+3; h++){ 262 | for(int w = 0; w < WIDTH; w++){ 263 | screen[h][w] = 0; 264 | } 265 | } 266 | counterID = 0; 267 | gameover = false; 268 | } 269 | else{ 270 | ledmatrix.gridFlush(); 271 | 272 | // list of all blocks in game, indicating which are moving 273 | // set every block on the screen as a potentially mover 274 | bool tomove[numBlocks+1]; 275 | for(int i = 0; i < numBlocks; i++) tomove[i+1] = i < counterID; 276 | 277 | // identify tiles which can move down (no collision below) 278 | for(int c = 0; c < WIDTH; c++){ // columns 279 | for(int r = 0; r < HEIGHT+3; r++){ // rows 280 | // only check pixels which are occupied 281 | if(screen[r][c] != 0){ 282 | // every tile which has a pixel in last row -> no mover 283 | if(r == HEIGHT+2){ 284 | tomove[screen[r][c]] = false; 285 | } 286 | // or every pixel 287 | else if(screen[r+1][c] != 0 && screen[r+1][c] != screen[r][c]){ 288 | tomove[screen[r][c]] = false; 289 | } 290 | } 291 | } 292 | } 293 | 294 | // indicate if there is no moving block 295 | // assume first there are no more moving block 296 | bool noMoreMover = true; 297 | // loop over existing block and ask if they can move 298 | for(int i = 0; i < counterID; i++){ 299 | if(tomove[i+1]){ 300 | noMoreMover = false; 301 | } 302 | } 303 | 304 | if(noMoreMover){ 305 | // no more moving blocks -> check if game over or spawn new block 306 | logger.logString("Tetris: No more Mover"); 307 | gameover = false; 308 | // check if game was lost -> one pixel active in 4rd row (top row on the led grid) 309 | for(int s = 0; s < WIDTH; s++){ 310 | if(screen[3][s] != 0) gameover = true; 311 | } 312 | if(gameover || counterID >= (numBlocks-1)){ 313 | logger.logString("Tetris: Gameover"); 314 | return 1; 315 | } 316 | 317 | // Create new block 318 | // increment counter 319 | counterID++; 320 | // select random shape for new block 321 | uint8_t randShape = random(1,9); 322 | // select random position (column) for spawn of new block 323 | uint8_t randx = random(0,WIDTH - 3); 324 | // copy shape to screen (c1 - column of block, c2 - column of screen) 325 | // write the id of block on the screen 326 | for(int c1 = 0, c2 = randx; c1 < 3; c1++, c2++){ 327 | for(int r = 0; r < 3; r++){ 328 | if(blockshapes[randShape][r][c1]) screen[r][c2] = counterID; 329 | } 330 | } 331 | } 332 | else{ 333 | uint8_t moveX = WIDTH-1; 334 | uint8_t moveY = HEIGHT+2; 335 | // moving blocks exists -> move them one pixel down 336 | // loop over pixels and move every pixel down, which belongs to a moving block 337 | for(int c = WIDTH-1; c >= 0; c--){ 338 | for(int r = HEIGHT+1; r >= 0; r--){ 339 | if((screen[r][c] != 0) && tomove[screen[r][c]]){ 340 | screen[r+1][c] = screen[r][c]; 341 | screen[r][c] = 0; 342 | // save top left corner of block 343 | if(moveX > c) moveX = c; 344 | if(moveY > r) moveY = r; 345 | } 346 | } 347 | } 348 | } 349 | 350 | // draw/copy screen values to led grid (r - row, c - column) 351 | for(int c = 0; c < WIDTH; c++){ 352 | for(int r = 0; r < HEIGHT; r++){ 353 | if(screen[r+3][c] != 0){ 354 | // screen is 3 pixels higher than led grid, so drop the upper three lines 355 | ledmatrix.gridAddPixel(c,r,colors24bit[(screen[r+3][c] % NUM_COLORS)]); 356 | } 357 | } 358 | } 359 | return 0; 360 | } 361 | return 0; 362 | } 363 | 364 | -------------------------------------------------------------------------------- /data/fs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Filesystem Manager 9 | 64 | 65 | 66 |

ESP8266 Filesystem Manager

67 |
68 | 69 | 70 |
71 |
72 | 73 | 74 |
75 |
76 |
77 | 78 |
79 | 80 | 81 | -------------------------------------------------------------------------------- /data/icons/all_icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 63 | 70 | 71 | 77 | 81 | 86 | 93 | 94 | 99 | 104 | 109 | 114 | 119 | 124 | 34 135 | 12 146 | 150 | 158 | 159 | 164 | 169 | 176 | 181 | 185 | 193 | 194 | 199 | 204 | 208 | 220 | 224 | 232 | 233 | 237 | 245 | 246 | 250 | 258 | 259 | 263 | 271 | 272 | 273 | 274 | -------------------------------------------------------------------------------- /data/icons/arrow_left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /data/icons/arrow_right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /data/icons/clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /data/icons/diclock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 34 16 | 12 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /data/icons/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /data/icons/pingpong.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /data/icons/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /data/icons/playpause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /data/icons/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /data/icons/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /data/icons/snake.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /data/icons/spiral.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /data/icons/tetris.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /data/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 265 | WORDCLOCK 2.0 266 | 267 | 268 | 269 | 270 |
271 | 272 |

WORDCLOCK 2.0

273 | 274 |
275 |
276 | 277 | 278 |
279 |
280 | 281 | 282 |
283 |
284 | 285 | 286 |
287 |
288 | 289 | 290 |
291 |
292 | 293 |
294 | 295 |
296 |
297 |
298 | 299 |
300 | 301 |
302 |
303 |
SAVE
304 |
305 | 306 |
307 |
308 |
309 | MODE 310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 | 321 |
322 | 323 |
324 |
325 |
326 | 327 |
328 | 329 |
330 |
331 |
332 | 333 |
334 | 335 |
336 |
337 | 338 | 361 | 362 | 363 | 390 | 391 | 392 | 420 | 421 | 444 | 445 | 446 | 638 | 639 | 640 | -------------------------------------------------------------------------------- /data/style.css: -------------------------------------------------------------------------------- 1 | 2 | /* For more information visit:https://fipsok.de */ 3 | body { 4 | font-family: sans-serif; 5 | background-color: #87cefa; 6 | display: flex; 7 | flex-flow: column; 8 | align-items: center; 9 | } 10 | h1,h2 { 11 | color: #e1e1e1; 12 | text-shadow: 2px 2px 2px black; 13 | } 14 | li { 15 | background-color: #feb1e2; 16 | list-style-type: none; 17 | margin-bottom: 10px; 18 | padding: 2px 5px 1px 0; 19 | box-shadow: 5px 5px 5px rgba(0,0,0,0.7); 20 | } 21 | li a:first-child, li b { 22 | background-color: #8f05a5; 23 | font-weight: bold; 24 | color: white; 25 | text-decoration:none; 26 | padding: 2px 5px; 27 | text-shadow: 2px 2px 1px black; 28 | cursor:pointer; 29 | } 30 | li strong { 31 | color: red; 32 | } 33 | input { 34 | height:35px; 35 | font-size:14px; 36 | padding-left: .3em; 37 | } 38 | label + a { 39 | text-decoration: none; 40 | } 41 | h1 + main { 42 | display: flex; 43 | } 44 | aside { 45 | display: flex; 46 | flex-direction: column; 47 | padding: 0.2em; 48 | } 49 | button { 50 | height:40px; 51 | width:130px; 52 | font-size:16px; 53 | margin-top: 1em; 54 | box-shadow: 5px 5px 5px rgba(0,0,0,0.7); 55 | } 56 | div button { 57 | background-color: #7bff97; 58 | } 59 | nav { 60 | display: flex; 61 | align-items: baseline; 62 | justify-content: space-between; 63 | } 64 | #left { 65 | align-items:flex-end; 66 | text-shadow: 0.5px 0.5px 1px #757474; 67 | } 68 | #cr { 69 | font-weight: bold; 70 | cursor:pointer; 71 | font-size: 1.5em; 72 | } 73 | #up { 74 | width: auto; 75 | } 76 | .note { 77 | background-color: #fecdee; 78 | padding: 0.5em; 79 | margin-top: 1em; 80 | text-align: center; 81 | max-width: 320px; 82 | border-radius: 0.5em; 83 | } 84 | .no { 85 | display: none; 86 | } 87 | form [title] { 88 | background-color: skyblue; 89 | font-size: 16px; 90 | width: 125px; 91 | } 92 | form:nth-of-type(2) { 93 | margin-bottom: 1em; 94 | } 95 | [value*=Format] { 96 | margin-top: 1em; 97 | box-shadow: 5px 5px 5px rgba(0,0,0,0.7); 98 | } 99 | [name="group"] { 100 | display: none; 101 | } 102 | [name="group"] + label { 103 | font-size: 1.5em; 104 | margin-right: 5px; 105 | } 106 | [name="group"] + label::before { 107 | content: "\002610"; 108 | } 109 | [name="group"]:checked + label::before { 110 | content: '\002611\0027A5'; 111 | } 112 | -------------------------------------------------------------------------------- /grid/grid_wordclock2.0_20x20cm.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techniccontroller/wordclock_esp8266/b0124723abbb33ed970b6f30361341b812e1b493/grid/grid_wordclock2.0_20x20cm.stl -------------------------------------------------------------------------------- /ledmatrix.cpp: -------------------------------------------------------------------------------- 1 | #include "ledmatrix.h" 2 | #include "own_font.h" 3 | 4 | /** 5 | * @brief Construct a new LEDMatrix::LEDMatrix object 6 | * 7 | * @param mymatrix pointer to Adafruit_NeoMatrix object 8 | * @param mybrightness the initial brightness of the leds 9 | * @param mylogger pointer to the UDPLogger object 10 | */ 11 | LEDMatrix::LEDMatrix(Adafruit_NeoMatrix *mymatrix, uint8_t mybrightness, UDPLogger *mylogger){ 12 | neomatrix = mymatrix; 13 | brightness = mybrightness; 14 | logger = mylogger; 15 | currentLimit = DEFAULT_CURRENT_LIMIT; 16 | } 17 | 18 | /** 19 | * @brief Convert RGB value to 24bit color value 20 | * 21 | * @param r red value (0-255) 22 | * @param g green value (0-255) 23 | * @param b blue value (0-255) 24 | * @return uint32_t 24bit color value 25 | */ 26 | uint32_t LEDMatrix::Color24bit(uint8_t r, uint8_t g, uint8_t b) 27 | { 28 | return ((uint32_t)r << 16) | ((uint32_t)g << 8) | b; 29 | } 30 | 31 | /** 32 | * @brief Convert 24bit color to 16bit color 33 | * 34 | * @param color24bit 24bit color value 35 | * @return uint16_t 16bit color value 36 | */ 37 | uint16_t LEDMatrix::color24to16bit(uint32_t color24bit){ 38 | uint8_t r = color24bit >> 16 & 0xff; 39 | uint8_t g = color24bit >> 8 & 0xff; 40 | uint8_t b = color24bit & 0xff; 41 | return ((uint16_t)(r & 0xF8) << 8) | 42 | ((uint16_t)(g & 0xFC) << 3) | 43 | (b >> 3); 44 | } 45 | 46 | /** 47 | * @brief Input a value 0 to 255 to get a color value. The colors are a transition r - g - b - back to r. 48 | * 49 | * @param WheelPos Value between 0 and 255 50 | * @return uint32_t return 24bit color of colorwheel 51 | */ 52 | uint32_t LEDMatrix::Wheel(uint8_t WheelPos) 53 | { 54 | WheelPos = 255 - WheelPos; 55 | if (WheelPos < 85) 56 | { 57 | return Color24bit(255 - WheelPos * 3, 0, WheelPos * 3); 58 | } 59 | if (WheelPos < 170) 60 | { 61 | WheelPos -= 85; 62 | return Color24bit(0, WheelPos * 3, 255 - WheelPos * 3); 63 | } 64 | WheelPos -= 170; 65 | return Color24bit(WheelPos * 3, 255 - WheelPos * 3, 0); 66 | } 67 | 68 | /** 69 | * @brief Interpolates two colors24bit and returns an color of the result 70 | * 71 | * @param color1 startcolor for interpolation 72 | * @param color2 endcolor for interpolatio 73 | * @param factor which color is wanted on the path from start to end color 74 | * @return uint32_t interpolated color 75 | */ 76 | uint32_t LEDMatrix::interpolateColor24bit(uint32_t color1, uint32_t color2, float factor) 77 | { 78 | uint8_t resultRed = color1 >> 16 & 0xff; 79 | uint8_t resultGreen = color1 >> 8 & 0xff; 80 | uint8_t resultBlue = color1 & 0xff; 81 | resultRed = (uint8_t)(resultRed + (int16_t)(factor * ((int16_t)(color2 >> 16 & 0xff) - (int16_t)resultRed))); 82 | resultGreen = (uint8_t)(resultGreen + (int16_t)(factor * ((int16_t)(color2 >> 8 & 0xff) - (int16_t)resultGreen))); 83 | resultBlue = (uint8_t)(resultBlue + (int16_t)(factor * ((int16_t)(color2 & 0xff) - (int16_t)resultBlue))); 84 | return Color24bit(resultRed, resultGreen, resultBlue); 85 | } 86 | 87 | /** 88 | * @brief Setup function for LED matrix 89 | * 90 | */ 91 | void LEDMatrix::setupMatrix() 92 | { 93 | (*neomatrix).begin(); 94 | (*neomatrix).setTextWrap(false); 95 | (*neomatrix).setBrightness(brightness); 96 | randomSeed(analogRead(0)); 97 | } 98 | 99 | /** 100 | * @brief Turn on the minutes indicator leds with the provided pattern (binary encoded) 101 | * 102 | * @param pattern the binary encoded pattern of the minute indicator 103 | * @param color color to be displayed 104 | */ 105 | void LEDMatrix::setMinIndicator(uint8_t pattern, uint32_t color) 106 | { 107 | if(dynamicColorShiftActivePhase >= 0){ 108 | color = Wheel(dynamicColorShiftActivePhase); 109 | } 110 | // pattern: 111 | // 15 -> 1111 112 | // 14 -> 1110 113 | // (...) 114 | // 2 -> 0010 115 | // 1 -> 0001 116 | // 0 -> 0000 117 | if(pattern & 1){ 118 | targetindicators[0] = color; 119 | } 120 | if(pattern >> 1 & 1){ 121 | targetindicators[1] = color; 122 | } 123 | if(pattern >> 2 & 1){ 124 | targetindicators[2] = color; 125 | } 126 | if(pattern >> 3 & 1){ 127 | targetindicators[3] = color; 128 | } 129 | } 130 | 131 | /** 132 | * @brief "Activates" a pixel in targetgrid with color 133 | * 134 | * @param x x-position of pixel 135 | * @param y y-position of pixel 136 | * @param color color of pixel 137 | */ 138 | void LEDMatrix::gridAddPixel(uint8_t x, uint8_t y, uint32_t color) 139 | { 140 | if(dynamicColorShiftActivePhase >= 0){ 141 | color = Wheel((uint16_t(x + y*WIDTH) * 256 * 2 / (WIDTH*HEIGHT) + dynamicColorShiftActivePhase) % 256); 142 | } 143 | // limit ranges of x and y 144 | if(x < WIDTH && y < HEIGHT){ 145 | targetgrid[y][x] = color; 146 | } 147 | else{ 148 | //logger->logString("Index out of Range: " + String(x) + ", " + String(y)); 149 | } 150 | } 151 | 152 | /** 153 | * @brief "Deactivates" all pixels in targetgrid 154 | * 155 | */ 156 | void LEDMatrix::gridFlush(void) 157 | { 158 | // set a zero to each pixel 159 | for(uint8_t i=0; i if yes reduce brightness 215 | if(totalCurrent > currentLimit){ 216 | uint8_t newBrightness = brightness * float(currentLimit)/float(totalCurrent); 217 | //logger->logString("CurrentLimit reached!!!: " + String(totalCurrent) + ", new: " + String(newBrightness)); 218 | (*neomatrix).setBrightness(newBrightness); 219 | } 220 | (*neomatrix).show(); 221 | } 222 | 223 | /** 224 | * @brief Shows a 1-digit number on LED matrix (5x3) 225 | * 226 | * @param xpos x of left top corner of digit 227 | * @param ypos y of left top corner of digit 228 | * @param number number to display 229 | * @param color color to display (24bit) 230 | */ 231 | void LEDMatrix::printNumber(uint8_t xpos, uint8_t ypos, uint8_t number, uint32_t color) 232 | { 233 | for(int y=ypos, i = 0; y < (ypos+5); y++, i++){ 234 | for(int x=xpos, k = 2; x < (xpos+3); x++, k--){ 235 | if((numbers_font[number][i] >> k) & 0x1){ 236 | gridAddPixel(x, y, color); 237 | } 238 | } 239 | } 240 | } 241 | 242 | /** 243 | * @brief Shows a character on LED matrix (5x3), supports currently only 'I' and 'P' 244 | * 245 | * @param xpos x of left top corner of character 246 | * @param ypos y of left top corner of character 247 | * @param character character to display 248 | * @param color color to display (24bit) 249 | */ 250 | void LEDMatrix::printChar(uint8_t xpos, uint8_t ypos, char character, uint32_t color) 251 | { 252 | int id = 0; 253 | if(character == 'I'){ 254 | id = 0; 255 | } 256 | else if(character == 'P'){ 257 | id = 1; 258 | } 259 | 260 | for(int y=ypos, i = 0; y < (ypos+5); y++, i++){ 261 | for(int x=xpos, k = 2; x < (xpos+3); x++, k--){ 262 | if((chars_font[id][i] >> k) & 0x1){ 263 | gridAddPixel(x, y, color); 264 | } 265 | } 266 | } 267 | } 268 | 269 | /** 270 | * @brief Set Brightness 271 | * 272 | * @param mybrightness brightness to be set [0..255] 273 | */ 274 | void LEDMatrix::setBrightness(uint8_t mybrightness){ 275 | brightness = mybrightness; 276 | (*neomatrix).setBrightness(brightness); 277 | } 278 | 279 | /** 280 | * @brief Calc estimated current (mA) for one pixel with the given color and brightness 281 | * 282 | * @param color 24bit color value of the pixel for which the current should be calculated 283 | * @return the current in mA 284 | */ 285 | uint16_t LEDMatrix::calcEstimatedLEDCurrent(uint32_t color){ 286 | // extract rgb values 287 | uint8_t red = color >> 16 & 0xff; 288 | uint8_t green = color >> 8 & 0xff; 289 | uint8_t blue = color & 0xff; 290 | 291 | // Linear estimation: 20mA for full brightness per LED 292 | // (calculation avoids float numbers) 293 | uint32_t estimatedCurrent = (20 * red) + (20 * green) + (20 * blue); 294 | estimatedCurrent /= 255; 295 | estimatedCurrent = (estimatedCurrent * brightness)/255; 296 | 297 | return estimatedCurrent; 298 | } 299 | 300 | /** 301 | * @brief Set the current limit 302 | * 303 | * @param mycurrentLimit the total current limit for whole matrix 304 | */ 305 | void LEDMatrix::setCurrentLimit(uint16_t mycurrentLimit){ 306 | currentLimit = mycurrentLimit; 307 | } 308 | 309 | /** 310 | * @brief Set dynamic color shift phase (0-255) 311 | * 312 | * @param phase phase of the color shift 313 | */ 314 | void LEDMatrix::setDynamicColorShiftPhase(int16_t phase) 315 | { 316 | dynamicColorShiftActivePhase = phase; 317 | } -------------------------------------------------------------------------------- /ledmatrix.h: -------------------------------------------------------------------------------- 1 | #ifndef ledmatrix_h 2 | #define ledmatrix_h 3 | 4 | #include 5 | #include 6 | #include 7 | #include "udplogger.h" 8 | 9 | // width of the led matrix 10 | #define WIDTH 11 11 | // height of the led matrix 12 | #define HEIGHT 11 13 | 14 | #define DEFAULT_CURRENT_LIMIT 9999 15 | 16 | class LEDMatrix{ 17 | public: 18 | LEDMatrix(Adafruit_NeoMatrix *mymatrix, uint8_t mybrightness, UDPLogger *mylogger); 19 | static uint32_t Color24bit(uint8_t r, uint8_t g, uint8_t b); 20 | static uint16_t color24to16bit(uint32_t color24bit); 21 | static uint32_t Wheel(uint8_t WheelPos); 22 | static uint32_t interpolateColor24bit(uint32_t color1, uint32_t color2, float factor); 23 | void setupMatrix(); 24 | void setMinIndicator(uint8_t pattern, uint32_t color); 25 | void gridAddPixel(uint8_t x, uint8_t y, uint32_t color); 26 | void gridFlush(void); 27 | void drawOnMatrixInstant(); 28 | void drawOnMatrixSmooth(float factor); 29 | void printNumber(uint8_t xpos, uint8_t ypos, uint8_t number, uint32_t color); 30 | void printChar(uint8_t xpos, uint8_t ypos, char character, uint32_t color); 31 | void setBrightness(uint8_t mybrightness); 32 | void setCurrentLimit(uint16_t mycurrentLimit); 33 | void setDynamicColorShiftPhase(int16_t phase); 34 | 35 | private: 36 | 37 | Adafruit_NeoMatrix *neomatrix; 38 | UDPLogger *logger; 39 | 40 | uint8_t brightness; 41 | uint16_t currentLimit; 42 | int16_t dynamicColorShiftActivePhase = -1; // -1: not active, 0-255: active phase shift 43 | 44 | // target representation of matrix as 2D array 45 | uint32_t targetgrid[HEIGHT][WIDTH] = {0}; 46 | 47 | // current representation of matrix as 2D array 48 | uint32_t currentgrid[HEIGHT][WIDTH] = {0}; 49 | 50 | // target representation of minutes indicator leds 51 | uint32_t targetindicators[4] = {0, 0, 0, 0}; 52 | 53 | // current representation of minutes indicator leds 54 | uint32_t currentindicators[4] = {0, 0, 0, 0}; 55 | 56 | void drawOnMatrix(float factor); 57 | uint16_t calcEstimatedLEDCurrent(uint32_t color); 58 | 59 | 60 | }; 61 | 62 | #endif -------------------------------------------------------------------------------- /multicastUDP_receiver.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import struct 3 | import sys 4 | from datetime import datetime 5 | import queue 6 | 7 | # ip address of network interface 8 | MCAST_IF_IP = '192.168.178.38' 9 | 10 | multicast_group = '230.120.10.2' 11 | server_address = ('', 8123) 12 | 13 | 14 | def start(filters=None): 15 | if filters is None: 16 | filters = [] 17 | 18 | # Create the socket 19 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 20 | 21 | # Bind to the server address 22 | sock.bind(server_address) 23 | 24 | print("Start") 25 | 26 | # Tell the operating system to add the socket to the multicast group on the specified interface 27 | group = socket.inet_aton(multicast_group) 28 | mreq = struct.pack('4s4s', group, socket.inet_aton(MCAST_IF_IP)) 29 | sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) 30 | 31 | print("Ready") 32 | 33 | # Initialize buffers and save counters for each filter 34 | buffers = {filter_val: queue.Queue(20) for filter_val in filters} 35 | save_counters = {filter_val: 0 for filter_val in filters} 36 | 37 | # Receive/respond loop 38 | while True: 39 | data, address = sock.recvfrom(1024) 40 | data_str = data.decode("utf-8").strip() 41 | timestamped_data = f"[{address[0]} - {datetime.now().strftime('%b-%d-%Y_%H:%M:%S')}] {data_str}" 42 | 43 | # Check each filter and process data accordingly 44 | for filter_val in filters: 45 | if filter_val in data_str: 46 | print(timestamped_data) 47 | buffers[filter_val].put(timestamped_data) 48 | if buffers[filter_val].full(): 49 | buffers[filter_val].get() 50 | 51 | # Save data if specific keywords are found or if save counter is active 52 | if "NTP-Update not successful" in data_str or "Start program" in data_str: 53 | with open(f"log_{filter_val}.txt", 'a') as f: 54 | while not buffers[filter_val].empty(): 55 | f.write(buffers[filter_val].get() + "\n") 56 | save_counters[filter_val] = 20 # Start the save counter 57 | 58 | if save_counters[filter_val] > 0: 59 | with open(f"log_{filter_val}.txt", 'a') as f: 60 | f.write(timestamped_data + "\n") 61 | if save_counters[filter_val] == 1: 62 | f.write("\n") 63 | save_counters[filter_val] -= 1 64 | 65 | 66 | # Main 67 | if __name__ == '__main__': 68 | # Check if filters are given 69 | # Use filters as arguments: python3 multicastUDP_receiver.py "filter1" "filter2" ... 70 | if len(sys.argv) > 1: 71 | start(sys.argv[1:]) 72 | else: 73 | start() 74 | -------------------------------------------------------------------------------- /ntp_client_plus.cpp: -------------------------------------------------------------------------------- 1 | #include "ntp_client_plus.h" 2 | 3 | /** 4 | * @brief Construct a new NTPClientPlus::NTPClientPlus object 5 | * 6 | * @param udp UDP client 7 | * @param poolServerName time server name 8 | * @param utcx UTC offset (in 1h) 9 | * @param _swChange should summer/winter time be considered 10 | */ 11 | NTPClientPlus::NTPClientPlus(UDP &udp, const char *poolServerName, int utcx, bool _swChange) 12 | { 13 | this->_udp = &udp; 14 | this->_utcx = utcx; 15 | this->_poolServerName = poolServerName; 16 | this->_swChange = _swChange; 17 | } 18 | 19 | /** 20 | * @brief Starts the underlying UDP client, get first NTP timestamp and calc date 21 | * 22 | */ 23 | void NTPClientPlus::setupNTPClient() 24 | { 25 | this->_udp->begin(this->_port); 26 | this->_udpSetup = true; 27 | this->updateNTP(); 28 | this->calcDate(); 29 | } 30 | 31 | /** 32 | * @brief Get new update from NTP 33 | * 34 | * @return 0 after successful update 35 | * @return -1 timeout after 1000 ms 36 | * @return 1 too much difference to previous received time (try again) 37 | */ 38 | int NTPClientPlus::updateNTP() 39 | { 40 | 41 | // flush any existing packets 42 | while (this->_udp->parsePacket() != 0) 43 | this->_udp->flush(); 44 | 45 | this->sendNTPPacket(); 46 | 47 | // Wait till data is there or timeout... 48 | byte timeout = 0; 49 | int cb = 0; 50 | do 51 | { 52 | delay(10); 53 | cb = this->_udp->parsePacket(); 54 | if (timeout > 100) 55 | return -1; // timeout after 1000 ms 56 | timeout++; 57 | } while (cb == 0); 58 | 59 | this->_udp->read(this->_packetBuffer, NTP_PACKET_SIZE); 60 | 61 | unsigned long highWord = word(this->_packetBuffer[40], this->_packetBuffer[41]); 62 | unsigned long lowWord = word(this->_packetBuffer[42], this->_packetBuffer[43]); 63 | // combine the four bytes (two words) into a long integer 64 | // this is NTP time (seconds since Jan 1 1900): 65 | unsigned long tempSecsSince1900 = highWord << 16 | lowWord; 66 | 67 | if(tempSecsSince1900 < SEVENZYYEARS){ 68 | // NTP time is not valid 69 | return 2; 70 | } 71 | 72 | // check if time off last ntp update is roughly in the same range: 100sec apart (validation check) 73 | if(this->_lastSecsSince1900 == 0 || tempSecsSince1900 - this->_lastSecsSince1900 < 100000){ 74 | // Only update time then 75 | this->_lastUpdate = millis() - (10 * (timeout + 1)); // Account for delay in reading the time 76 | 77 | this->_secsSince1900 = tempSecsSince1900; 78 | 79 | this->_currentEpoc = this->_secsSince1900 - SEVENZYYEARS; 80 | 81 | // Remember time of last update 82 | this->_lastSecsSince1900 = tempSecsSince1900; 83 | 84 | return 0; // return 0 after successful update 85 | } 86 | else{ 87 | // Remember time of last update 88 | this->_lastSecsSince1900 = tempSecsSince1900; 89 | 90 | return 1; 91 | } 92 | } 93 | 94 | /** 95 | * @brief Stops the underlying UDP client 96 | * 97 | */ 98 | void NTPClientPlus::end() 99 | { 100 | this->_udp->stop(); 101 | 102 | this->_udpSetup = false; 103 | } 104 | 105 | /** 106 | * @brief Setter UTC offset 107 | * 108 | * @param utcOffset offset from UTC in minutes 109 | */ 110 | void NTPClientPlus::setUTCOffset(int utcOffset) 111 | { 112 | this->_utcx = utcOffset; 113 | } 114 | 115 | /** 116 | * @brief Set time server name 117 | * 118 | * @param poolServerName 119 | */ 120 | void NTPClientPlus::setPoolServerName(const char *poolServerName) 121 | { 122 | this->_poolServerName = poolServerName; 123 | } 124 | 125 | /** 126 | * @brief Calc seconds since 1. Jan. 1900 127 | * 128 | * @return unsigned long seconds since 1. Jan. 1900 129 | */ 130 | unsigned long NTPClientPlus::getSecsSince1900() const 131 | { 132 | return this->_utcx * this->secondperminute + // UTC offset 133 | this->_summertime * this->secondperhour + // Summer time offset 134 | this->_secsSince1900 + // seconds returned by the NTP server 135 | ((millis() - this->_lastUpdate) / 1000); // Time since last update 136 | } 137 | 138 | /** 139 | * @brief Get UNIX Epoch time since 1. Jan. 1970 140 | * 141 | * @return unsigned long UNIX Epoch time since 1. Jan. 1970 in seconds 142 | */ 143 | unsigned long NTPClientPlus::getEpochTime() const 144 | { 145 | return this->getSecsSince1900() - SEVENZYYEARS; 146 | } 147 | 148 | /** 149 | * @brief Get current hours in 24h format 150 | * 151 | * @return int 152 | */ 153 | int NTPClientPlus::getHours24() const 154 | { 155 | int hours = ((this->getEpochTime() % 86400L) / 3600); 156 | return hours; 157 | } 158 | 159 | /** 160 | * @brief Get current hours in 12h format 161 | * 162 | * @return int 163 | */ 164 | int NTPClientPlus::getHours12() const 165 | { 166 | int hours = this->getHours24(); 167 | if (hours >= 12) 168 | { 169 | hours = hours - 12; 170 | } 171 | return hours; 172 | } 173 | 174 | /** 175 | * @brief Get current minutes 176 | * 177 | * @return int 178 | */ 179 | int NTPClientPlus::getMinutes() const 180 | { 181 | return ((this->getEpochTime() % 3600) / 60); 182 | } 183 | 184 | /** 185 | * @brief Get current seconds 186 | * 187 | * @return int 188 | */ 189 | int NTPClientPlus::getSeconds() const 190 | { 191 | return (this->getEpochTime() % 60); 192 | } 193 | 194 | /** 195 | * @brief 196 | * 197 | * @return String time formatted like `hh:mm:ss` 198 | */ 199 | String NTPClientPlus::getFormattedTime() const { 200 | unsigned long rawTime = this->getEpochTime(); 201 | unsigned long hours = (rawTime % 86400L) / 3600; 202 | String hoursStr = hours < 10 ? "0" + String(hours) : String(hours); 203 | 204 | unsigned long minutes = (rawTime % 3600) / 60; 205 | String minuteStr = minutes < 10 ? "0" + String(minutes) : String(minutes); 206 | 207 | unsigned long seconds = rawTime % 60; 208 | String secondStr = seconds < 10 ? "0" + String(seconds) : String(seconds); 209 | 210 | return hoursStr + ":" + minuteStr + ":" + secondStr; 211 | } 212 | 213 | /** 214 | * @brief 215 | * 216 | * @return String date formatted like `dd.mm.yyyy` 217 | */ 218 | String NTPClientPlus::getFormattedDate() { 219 | this->calcDate(); 220 | unsigned int dateDay = this->_dateDay; 221 | unsigned int dateMonth = this->_dateMonth; 222 | unsigned int dateYear = this->_dateYear; 223 | 224 | String dayStr = dateDay < 10 ? "0" + String(dateDay) : String(dateDay); 225 | String monthStr = dateMonth < 10 ? "0" + String(dateMonth) : String(dateMonth); 226 | String yearStr = dateYear < 10 ? "0" + String(dateYear) : String(dateYear); 227 | 228 | return dayStr + "." + monthStr + "." + yearStr; 229 | } 230 | 231 | 232 | /** 233 | * @brief Calc date from seconds since 1900 234 | * 235 | */ 236 | void NTPClientPlus::calcDate() 237 | { 238 | // Start: Calc date 239 | 240 | // get days since 1900 241 | unsigned long days1900 = this->getSecsSince1900() / secondperday; 242 | 243 | // calc current year 244 | this->_dateYear = this->getYear(); 245 | 246 | // calc how many leap days since 1.Jan 1900 247 | int leapDays = 0; 248 | for (unsigned int i = 1900; i < this->_dateYear; i++) 249 | { 250 | // check if leap year 251 | if (this->isLeapYear(i)) 252 | { 253 | leapDays++; 254 | } 255 | } 256 | leapDays = leapDays - 1; 257 | 258 | // check if current year is leap year 259 | if (this->isLeapYear(this->_dateYear)) 260 | { 261 | daysInMonth[2] = 29; 262 | } 263 | else 264 | { 265 | daysInMonth[2] = 28; 266 | } 267 | 268 | unsigned int dayOfYear = (days1900 - ((this->_dateYear - 1900) * 365) - leapDays); 269 | 270 | // calc current month 271 | this->_dateMonth = this->getMonth(dayOfYear); 272 | 273 | this->_dateDay = 0; 274 | 275 | // calc day of month 276 | for (unsigned int i = 0; i < this->_dateMonth; i++) 277 | { 278 | this->_dateDay = this->_dateDay + daysInMonth[i]; 279 | } 280 | this->_dateDay = dayOfYear - this->_dateDay; 281 | 282 | // calc day of week: 283 | // Monday = 1, Tuesday = 2, Wednesday = 3, Thursday = 4, Friday = 5, Saturday = 6, Sunday = 7 284 | // 1. Januar 1900 was a monday 285 | this->_dayOfWeek = 1; 286 | 287 | for (unsigned long i = 0; i < days1900; i++) 288 | { 289 | 290 | if (this->_dayOfWeek < 7) 291 | { 292 | this->_dayOfWeek = this->_dayOfWeek + 1; 293 | } 294 | else 295 | { 296 | this->_dayOfWeek = 1; 297 | } 298 | } 299 | 300 | // End: Calc date (dateDay, dateMonth, dateYear) 301 | 302 | // calc if summer time active 303 | 304 | this->updateSWChange(); 305 | } 306 | 307 | /** 308 | * @brief Getter for day of the week 309 | * 310 | * @return unsigned int 311 | */ 312 | unsigned int NTPClientPlus::getDayOfWeek() 313 | { 314 | return this->_dayOfWeek; 315 | } 316 | 317 | /** 318 | * @brief Function to calc current year 319 | * 320 | * @return unsigned int 321 | */ 322 | unsigned int NTPClientPlus::getYear() 323 | { 324 | 325 | unsigned long sec1900 = this->getSecsSince1900(); 326 | 327 | //NTP starts at 1. Jan 1900 328 | unsigned int result = 1900; 329 | unsigned int dayInYear = 0; 330 | unsigned int days = 0; 331 | unsigned int days1900 = 0; 332 | 333 | unsigned long for_i = 0; 334 | bool leapYear = LOW; 335 | 336 | days1900 = sec1900 / this->secondperday; 337 | 338 | for (for_i = 0; for_i < days1900; for_i++) 339 | { 340 | 341 | leapYear = this->isLeapYear(result); 342 | 343 | if (leapYear) 344 | { 345 | dayInYear = 366; 346 | } 347 | 348 | else 349 | { 350 | dayInYear = 365; 351 | } 352 | 353 | days++; 354 | 355 | if (days >= dayInYear) 356 | { 357 | result++; 358 | days = 0; 359 | } 360 | } 361 | 362 | return result; 363 | } 364 | 365 | /** 366 | * @brief Function to check if given year is leap year 367 | * 368 | * @param year 369 | * @return true 370 | * @return false 371 | */ 372 | bool NTPClientPlus::isLeapYear(unsigned int year) 373 | { 374 | 375 | bool result = LOW; 376 | 377 | // check for leap year 378 | if ((year % 4) == 0) 379 | { 380 | 381 | result = HIGH; 382 | 383 | if ((year % 100) == 0) 384 | { 385 | 386 | result = LOW; 387 | 388 | if ((year % 400) == 0) 389 | { 390 | 391 | result = HIGH; 392 | } 393 | } 394 | } 395 | 396 | else 397 | { 398 | result = LOW; 399 | } 400 | 401 | return result; 402 | } 403 | 404 | /** 405 | * @brief Get Month of given day of year 406 | * 407 | * @param dayOfYear 408 | * @return int 409 | */ 410 | int NTPClientPlus::getMonth(int dayOfYear) 411 | { 412 | 413 | bool leapYear = this->isLeapYear(this->getYear()); 414 | 415 | // Month beginnings 416 | int monthMin[13] = {0, 1, 32, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335}; 417 | // Month endings 418 | int monthMax[13] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365}; 419 | 420 | int month = 0; 421 | 422 | int y = 0; 423 | 424 | // Calculation of the beginning and end of each month in the leap year 425 | if (leapYear == HIGH) 426 | { 427 | 428 | for (y = 3; y < 13; y++) 429 | { 430 | monthMin[y] = monthMin[y] + 1; 431 | } 432 | 433 | for (y = 2; y < 13; y++) 434 | { 435 | monthMax[y] = monthMax[y] + 1; 436 | } 437 | } 438 | 439 | // January 440 | if (dayOfYear >= monthMin[1] && dayOfYear <= monthMax[1]) 441 | { 442 | month = 1; 443 | } 444 | 445 | // February 446 | if (dayOfYear >= monthMin[2] && dayOfYear <= monthMax[2]) 447 | { 448 | month = 2; 449 | } 450 | 451 | // March 452 | if (dayOfYear >= monthMin[3] && dayOfYear <= monthMax[3]) 453 | { 454 | month = 3; 455 | } 456 | 457 | // April 458 | if (dayOfYear >= monthMin[4] && dayOfYear <= monthMax[4]) 459 | { 460 | month = 4; 461 | } 462 | 463 | // May 464 | if (dayOfYear >= monthMin[5] && dayOfYear <= monthMax[5]) 465 | { 466 | month = 5; 467 | } 468 | 469 | // June 470 | if (dayOfYear >= monthMin[6] && dayOfYear <= monthMax[6]) 471 | { 472 | month = 6; 473 | } 474 | 475 | // July 476 | if (dayOfYear >= monthMin[7] && dayOfYear <= monthMax[7]) 477 | { 478 | month = 7; 479 | } 480 | 481 | // August 482 | if (dayOfYear >= monthMin[8] && dayOfYear <= monthMax[8]) 483 | { 484 | month = 8; 485 | } 486 | 487 | // September 488 | if (dayOfYear >= monthMin[9] && dayOfYear <= monthMax[9]) 489 | { 490 | month = 9; 491 | } 492 | 493 | // October 494 | if (dayOfYear >= monthMin[10] && dayOfYear <= monthMax[10]) 495 | { 496 | month = 10; 497 | } 498 | 499 | // November 500 | if (dayOfYear >= monthMin[11] && dayOfYear <= monthMax[11]) 501 | { 502 | month = 11; 503 | } 504 | 505 | // December 506 | if (dayOfYear >= monthMin[12] && dayOfYear <= monthMax[12]) 507 | { 508 | month = 12; 509 | } 510 | 511 | return month; 512 | } 513 | 514 | /** 515 | * @brief (private) Send NTP Packet to NTP server 516 | * 517 | */ 518 | void NTPClientPlus::sendNTPPacket() 519 | { 520 | // set all bytes in the buffer to 0 521 | memset(this->_packetBuffer, 0, NTP_PACKET_SIZE); 522 | // Initialize values needed to form NTP request 523 | this->_packetBuffer[0] = 0b11100011; // LI, Version, Mode 524 | this->_packetBuffer[1] = 0; // Stratum, or type of clock 525 | this->_packetBuffer[2] = 6; // Polling Interval 526 | this->_packetBuffer[3] = 0xEC; // Peer Clock Precision 527 | // 8 bytes of zero for Root Delay & Root Dispersion 528 | this->_packetBuffer[12] = 49; 529 | this->_packetBuffer[13] = 0x4E; 530 | this->_packetBuffer[14] = 49; 531 | this->_packetBuffer[15] = 52; 532 | 533 | // all NTP fields have been given values, now 534 | // you can send a packet requesting a timestamp: 535 | if (this->_poolServerName) 536 | { 537 | this->_udp->beginPacket(this->_poolServerName, 123); 538 | } 539 | else 540 | { 541 | this->_udp->beginPacket(this->_poolServerIP, 123); 542 | } 543 | this->_udp->write(this->_packetBuffer, NTP_PACKET_SIZE); 544 | this->_udp->endPacket(); 545 | } 546 | 547 | /** 548 | * @brief (private) Set time offset accordance to summer time 549 | * 550 | * @param summertime 551 | */ 552 | void NTPClientPlus::setSummertime(bool summertime) 553 | { 554 | this->_summertime = summertime; 555 | } 556 | 557 | /** 558 | * @brief Update Summer/Winter time change 559 | * 560 | * @returns bool summertime active 561 | */ 562 | bool NTPClientPlus::updateSWChange() 563 | { 564 | unsigned int dayOfWeek = this->_dayOfWeek; 565 | unsigned int dateDay = this->_dateDay; 566 | unsigned int dateMonth = this->_dateMonth; 567 | 568 | bool summertimeActive = false; 569 | 570 | if (this->_swChange) 571 | { 572 | //Start: Set summer-/ winter time 573 | 574 | // current month is march 575 | if (dateMonth == 3) 576 | { 577 | 578 | // it is last week in march 579 | if ((this->daysInMonth[3] - dateDay) < 7) 580 | { 581 | 582 | //Example year 2020: March 31 days; Check on March 26, 2020 (Thursday = weekday = 4); 5 days remaining; Last Sunday March 29, 2020 583 | //Calculation: 31 - 26 = 5; 5 + 4 = 9; 584 | //Result: Last day in March is a Tuesday. There follows another Sunday in October => set winter time 585 | 586 | //Example year 2021: March 31 days; Check on March 30, 2021 (Tuesday = weekday = 2); 1 days remaining; Last Sunday March 28, 2021 587 | //Calculation: 31 - 30 = 1; 1 + 2 = 3; 588 | //Result: Last day in March is a Wednesday. Changeover to summer time already done => set summer time 589 | 590 | // If today is Sunday (dayOfWeek == 7) then this is already the last sunday in march -> set summer time 591 | if(dayOfWeek == 7){ 592 | this->setSummertime(1); 593 | summertimeActive = true; 594 | } 595 | 596 | //There follows within the last week in March one more Sunday => set winter time 597 | else if (((this->daysInMonth[3] - dateDay) + dayOfWeek) >= 7) 598 | { 599 | this->setSummertime(0); 600 | summertimeActive = false; 601 | } 602 | 603 | // last sunday in march already over -> set summer time 604 | else 605 | { 606 | this->setSummertime(1); 607 | summertimeActive = true; 608 | } 609 | } 610 | 611 | // Check in first three weeks of march -> winter time 612 | else 613 | { 614 | this->setSummertime(0); 615 | summertimeActive = false; 616 | } 617 | } 618 | 619 | // current month is october 620 | else if (dateMonth == 10) 621 | { 622 | 623 | // Check in last week of october 624 | if ((this->daysInMonth[10] - dateDay) < 7) 625 | { 626 | 627 | //Example year 2020: October 31 days; Check on October 26, 2020 (Monday = weekday = 1); 5 days remaining; last Sunday October 25, 2020 628 | //Calculation: 31 - 26 = 5; 5 + 1 = 6; 629 | //Result: Last day in October is a Saturday. Changeover to winter time already done => set winter time 630 | 631 | //Example year 2021: October 31 days; Check on 26. October 2021 (Tuesday = weekday = 2); 5 days remaining; Last Sunday 31. October 2021 632 | //Calculation: 31 - 26 = 5; 5 + 2 = 7; 633 | //Result: Last day in October is a Sunday. There follows another Sunday in October => set summer time 634 | 635 | // If today is Sunday (dayOfWeek == 7) then this is already the last sunday in october -> winter time 636 | if(dayOfWeek == 7){ 637 | this->setSummertime(0); 638 | summertimeActive = false; 639 | } 640 | 641 | // There follows within the last week in October one more Sunday => summer time 642 | else if (((this->daysInMonth[10] - dateDay) + dayOfWeek) >= 7) 643 | { 644 | this->setSummertime(1); 645 | summertimeActive = true; 646 | } 647 | 648 | // last sunday in october already over -> winter time 649 | else 650 | { 651 | this->setSummertime(0); 652 | summertimeActive = false; 653 | } 654 | } 655 | 656 | // Check in first three weeks of october -> summer time 657 | else 658 | { 659 | this->setSummertime(1); 660 | summertimeActive = true; 661 | } 662 | } 663 | 664 | // Check in summer time 665 | else if (dateMonth > 3 && dateMonth < 10) 666 | { 667 | this->setSummertime(1); 668 | summertimeActive = true; 669 | } 670 | 671 | // Check in winter time 672 | else if (dateMonth < 3 || dateMonth > 10) 673 | { 674 | this->setSummertime(0); 675 | summertimeActive = false; 676 | } 677 | } 678 | 679 | return summertimeActive; 680 | } -------------------------------------------------------------------------------- /ntp_client_plus.h: -------------------------------------------------------------------------------- 1 | #ifndef ntpclientplus_h 2 | #define ntpclientplus_h 3 | 4 | #include 5 | #include 6 | 7 | #define SEVENZYYEARS 2208988800UL 8 | #define NTP_PACKET_SIZE 48 9 | #define NTP_DEFAULT_LOCAL_PORT 1337 10 | 11 | /** 12 | * @brief Own NTP Client library for Arduino with code from: 13 | * - https://github.com/arduino-libraries/NTPClient 14 | * - SPS&Technik - Projekt WordClock v1.02 15 | * 16 | */ 17 | class NTPClientPlus{ 18 | 19 | public: 20 | NTPClientPlus(UDP &udp, const char* poolServerName, int utcx, bool _swChange); 21 | void setupNTPClient(); 22 | int updateNTP(); 23 | void end(); 24 | void setUTCOffset(int utcOffset); 25 | void setPoolServerName(const char* poolServerName); 26 | unsigned long getSecsSince1900() const; 27 | unsigned long getEpochTime() const; 28 | int getHours24() const; 29 | int getHours12() const; 30 | int getMinutes() const; 31 | int getSeconds() const; 32 | String getFormattedTime() const; 33 | String getFormattedDate(); 34 | void calcDate(); 35 | unsigned int getDayOfWeek(); 36 | unsigned int getYear(); 37 | bool isLeapYear(unsigned int year); 38 | int getMonth(int dayOfYear); 39 | bool updateSWChange(); 40 | 41 | 42 | private: 43 | UDP* _udp; 44 | bool _udpSetup = false; 45 | 46 | const char* _poolServerName = "pool.ntp.org"; // Default time server 47 | IPAddress _poolServerIP; 48 | unsigned int _port = NTP_DEFAULT_LOCAL_PORT; 49 | int _utcx = 0; 50 | bool _swChange = 1; 51 | bool _summertime = false; 52 | 53 | unsigned long _updateInterval = 60000; // In ms 54 | 55 | unsigned long _currentEpoc = 0; // In s 56 | unsigned long _lastUpdate = 0; // In ms 57 | unsigned long _secsSince1900 = 0; // seconds since 1. Januar 1900, 00:00:00 58 | unsigned long _lastSecsSince1900 = 0; 59 | unsigned int _dateYear = 0; 60 | unsigned int _dateMonth = 0; 61 | unsigned int _dateDay = 0; 62 | unsigned int _dayOfWeek = 0; 63 | 64 | 65 | byte _packetBuffer[NTP_PACKET_SIZE]; 66 | void sendNTPPacket(); 67 | void setSummertime(bool summertime); 68 | 69 | 70 | static const unsigned long secondperday = 86400; 71 | static const unsigned long secondperhour = 3600; 72 | static const unsigned long secondperminute = 60; 73 | static const unsigned long minuteperhour = 60; 74 | static const unsigned long millisecondpersecond = 1000; 75 | 76 | // number of days in months 77 | unsigned int daysInMonth[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; 78 | 79 | 80 | 81 | 82 | }; 83 | 84 | 85 | 86 | 87 | #endif -------------------------------------------------------------------------------- /otafunctions.ino: -------------------------------------------------------------------------------- 1 | // setup Arduino OTA 2 | void setupOTA(String hostname){ 3 | // Port defaults to 8266 4 | // ArduinoOTA.setPort(8266); 5 | 6 | // Hostname defaults to esp8266-[ChipID] 7 | ArduinoOTA.setHostname(hostname.c_str()); 8 | 9 | // No authentication by default 10 | // ArduinoOTA.setPassword("admin"); 11 | 12 | // Password can be set with it's md5 value as well 13 | // MD5(admin) = 21232f297a57a5a743894a0e4a801fc3 14 | // ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3"); 15 | 16 | ArduinoOTA.onStart([]() { 17 | String type; 18 | if (ArduinoOTA.getCommand() == U_FLASH) { 19 | type = "sketch"; 20 | } else { // U_FS 21 | type = "filesystem"; 22 | } 23 | 24 | // NOTE: if updating FS this would be the place to unmount FS using FS.end() 25 | //Serial.println("Start updating " + type); 26 | }); 27 | ArduinoOTA.onEnd([]() { 28 | //Serial.println("\nEnd"); 29 | }); 30 | ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { 31 | //Serial.printf("Progress: %u%%\r", (progress / (total / 100))); 32 | }); 33 | ArduinoOTA.onError([](ota_error_t error) { 34 | //Serial.printf("Error[%u]: ", error); 35 | if (error == OTA_AUTH_ERROR) { 36 | //Serial.println("Auth Failed"); 37 | } else if (error == OTA_BEGIN_ERROR) { 38 | //Serial.println("Begin Failed"); 39 | } else if (error == OTA_CONNECT_ERROR) { 40 | //Serial.println("Connect Failed"); 41 | } else if (error == OTA_RECEIVE_ERROR) { 42 | //Serial.println("Receive Failed"); 43 | } else if (error == OTA_END_ERROR) { 44 | //Serial.println("End Failed"); 45 | } 46 | }); 47 | ArduinoOTA.begin(); 48 | } 49 | 50 | void handleOTA(){ 51 | // handle OTA 52 | ArduinoOTA.handle(); 53 | } -------------------------------------------------------------------------------- /own_font.h: -------------------------------------------------------------------------------- 1 | #ifndef ownfont_h 2 | #define ownfont_h 3 | 4 | uint8_t numbers_font[10][5] = { {0b00000111, 5 | 0b00000101, 6 | 0b00000101, 7 | 0b00000101, 8 | 0b00000111}, 9 | {0b00000001, 10 | 0b00000001, 11 | 0b00000001, 12 | 0b00000001, 13 | 0b00000001}, 14 | {0b00000111, 15 | 0b00000001, 16 | 0b00000111, 17 | 0b00000100, 18 | 0b00000111}, 19 | {0b00000111, 20 | 0b00000001, 21 | 0b00000111, 22 | 0b00000001, 23 | 0b00000111}, 24 | {0b00000101, 25 | 0b00000101, 26 | 0b00000111, 27 | 0b00000001, 28 | 0b00000001}, 29 | {0b00000111, 30 | 0b00000100, 31 | 0b00000111, 32 | 0b00000001, 33 | 0b00000111}, 34 | {0b00000111, 35 | 0b00000100, 36 | 0b00000111, 37 | 0b00000101, 38 | 0b00000111}, 39 | {0b00000111, 40 | 0b00000001, 41 | 0b00000001, 42 | 0b00000001, 43 | 0b00000001}, 44 | {0b00000111, 45 | 0b00000101, 46 | 0b00000111, 47 | 0b00000101, 48 | 0b00000111}, 49 | {0b00000111, 50 | 0b00000101, 51 | 0b00000111, 52 | 0b00000001, 53 | 0b00000111}}; 54 | 55 | uint8_t chars_font[2][5] = { {0b00000010, 56 | 0b00000010, 57 | 0b00000010, 58 | 0b00000010, 59 | 0b00000010}, 60 | {0b00000111, 61 | 0b00000101, 62 | 0b00000111, 63 | 0b00000100, 64 | 0b00000100}}; 65 | 66 | #endif -------------------------------------------------------------------------------- /pong.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file pong.cpp 3 | * @author techniccontroller (mail[at]techniccontroller.com) 4 | * @brief Class implementation for pong game 5 | * @version 0.1 6 | * @date 2022-03-06 7 | * 8 | * @copyright Copyright (c) 2022 9 | * 10 | * main code from https://elektro.turanis.de/html/prj041/index.html 11 | * 12 | */ 13 | #include "pong.h" 14 | 15 | /** 16 | * @brief Construct a new Pong:: Pong object 17 | * 18 | */ 19 | Pong::Pong(){ 20 | 21 | } 22 | 23 | /** 24 | * @brief Construct a new Pong:: Pong object 25 | * 26 | * @param myledmatrix pointer to LEDMatrix object, need to provide gridAddPixel(x, y, col), gridFlush() 27 | * @param mylogger pointer to UDPLogger object, need to provide a function logString(message) 28 | */ 29 | Pong::Pong(LEDMatrix *myledmatrix, UDPLogger *mylogger){ 30 | _ledmatrix = myledmatrix; 31 | _logger = mylogger; 32 | _gameState = GAME_STATE_END; 33 | } 34 | 35 | /** 36 | * @brief Run main loop for one cycle 37 | * 38 | */ 39 | void Pong::loopCycle(){ 40 | switch(_gameState) { 41 | case GAME_STATE_INIT: 42 | initGame(2); 43 | break; 44 | case GAME_STATE_RUNNING: 45 | updateBall(); 46 | updateGame(); 47 | break; 48 | case GAME_STATE_END: 49 | break; 50 | } 51 | } 52 | 53 | /** 54 | * @brief Trigger control: UP for given player 55 | * 56 | * @param playerid id of player {0, 1} 57 | */ 58 | void Pong::ctrlUp(uint8_t playerid){ 59 | if (millis() > _lastButtonClick + DEBOUNCE_TIME_PONG) { 60 | _playerMovement[playerid] = PADDLE_MOVE_DOWN; // need to swap direction as field is rotated 180deg 61 | _lastButtonClick = millis(); 62 | } 63 | } 64 | 65 | /** 66 | * @brief Trigger control: DOWN for given player 67 | * 68 | * @param playerid id of player {0, 1} 69 | */ 70 | void Pong::ctrlDown(uint8_t playerid){ 71 | if (millis() > _lastButtonClick + DEBOUNCE_TIME_PONG) { 72 | _playerMovement[playerid] = PADDLE_MOVE_UP; // need to swap direction as field is rotated 180deg 73 | _lastButtonClick = millis(); 74 | } 75 | } 76 | 77 | /** 78 | * @brief Trigger control: NONE for given player 79 | * 80 | * @param playerid id of player {0, 1} 81 | */ 82 | void Pong::ctrlNone(uint8_t playerid){ 83 | if (millis() > _lastButtonClick + DEBOUNCE_TIME_PONG) { 84 | _playerMovement[playerid] = PADDLE_MOVE_NONE; 85 | _lastButtonClick = millis(); 86 | } 87 | } 88 | 89 | /** 90 | * @brief Initialize a new game 91 | * 92 | * @param numBots number of bots {0, 1, 2} -> two bots results in animation 93 | */ 94 | void Pong::initGame(uint8_t numBots) 95 | { 96 | (*_logger).logString("Pong: init with " + String(numBots) + " Bots"); 97 | resetLEDs(); 98 | _lastButtonClick = millis(); 99 | 100 | _numBots = numBots; 101 | 102 | _ball.x = 1; 103 | _ball.y = (Y_MAX/2) - (PADDLE_WIDTH/2) + 1; 104 | _ball_old.x = _ball.x; 105 | _ball_old.y = _ball.y; 106 | _ballMovement[0] = 1; 107 | _ballMovement[1] = -1; 108 | _ballDelay = BALL_DELAY_MAX; 109 | 110 | for(uint8_t i=0; i BALL_DELAY_MIN) { 156 | _ballDelay -= BALL_DELAY_STEP; 157 | } 158 | } 159 | 160 | _ball.x += _ballMovement[0]; 161 | _ball.y += _ballMovement[1]; 162 | 163 | if (_ball.x <=0 || _ball.x >= X_MAX-1) { 164 | endGame(); 165 | return; 166 | } 167 | 168 | if (_ball.y <= 0 || _ball.y >= Y_MAX-1) { 169 | _ballMovement[1] *= -1; 170 | } 171 | 172 | toggleLed(_ball.x, _ball.y, LED_TYPE_BALL); 173 | } 174 | 175 | /** 176 | * @brief Game over, draw ball red 177 | * 178 | */ 179 | void Pong::endGame() 180 | { 181 | (*_logger).logString("Pong: Game ended"); 182 | _gameState = GAME_STATE_END; 183 | toggleLed(_ball.x, _ball.y, LED_TYPE_BALL_RED); 184 | } 185 | 186 | /** 187 | * @brief Update paddle position and check for game over 188 | * 189 | */ 190 | void Pong::updateGame() 191 | { 192 | if ((millis() - _lastDrawUpdate) < GAME_DELAY_PONG) { 193 | return; 194 | } 195 | _lastDrawUpdate = millis(); 196 | 197 | // turn off paddle LEDs 198 | for(uint8_t p=0; p 0) { 213 | for(uint8_t i=0; i 0 && playerId == 0) || (_ballMovement[0] < 0 && playerId == 1)){ 242 | action = PADDLE_MOVE_NONE; 243 | } 244 | else if(diff > 0){ 245 | action = PADDLE_MOVE_DOWN; 246 | } 247 | else{ 248 | action = PADDLE_MOVE_UP; 249 | } 250 | } 251 | else{ 252 | action = _playerMovement[playerId]; 253 | _playerMovement[playerId] = PADDLE_MOVE_NONE; 254 | } 255 | return action; 256 | } 257 | 258 | /** 259 | * @brief Clear the led matrix (turn all leds off) 260 | * 261 | */ 262 | void Pong::resetLEDs() 263 | { 264 | (*_ledmatrix).gridFlush(); 265 | } 266 | 267 | /** 268 | * @brief Turn on LED on matrix 269 | * 270 | * @param x x position of led 271 | * @param y y position of led 272 | * @param type type of pixel {PADDLE, BALL_RED, BALL, OFF} 273 | */ 274 | void Pong::toggleLed(uint8_t x, uint8_t y, uint8_t type) 275 | { 276 | uint32_t color = LEDMatrix::Color24bit(0, 0, 0); 277 | 278 | switch(type) { 279 | case LED_TYPE_PADDLE: 280 | color = LEDMatrix::Color24bit(0, 80, 80); 281 | break; 282 | case LED_TYPE_BALL_RED: 283 | color = LEDMatrix::Color24bit(120, 0, 0); 284 | break; 285 | case LED_TYPE_BALL: 286 | color = LEDMatrix::Color24bit(0, 100, 0); 287 | break; 288 | case LED_TYPE_OFF: 289 | color = LEDMatrix::Color24bit(0, 0, 0); 290 | break; 291 | } 292 | 293 | (*_ledmatrix).gridAddPixel(x, y, color); 294 | } -------------------------------------------------------------------------------- /pong.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file pong.h 3 | * @author techniccontroller (mail[at]techniccontroller.com) 4 | * @brief Class declaration for pong game 5 | * @version 0.1 6 | * @date 2022-03-05 7 | * 8 | * @copyright Copyright (c) 2022 9 | * 10 | * main code from https://elektro.turanis.de/html/prj041/index.html 11 | * 12 | */ 13 | 14 | #ifndef pong_h 15 | #define pong_h 16 | 17 | #include 18 | #include "ledmatrix.h" 19 | #include "udplogger.h" 20 | 21 | #define DEBOUNCE_TIME_PONG 10 // in ms 22 | 23 | #define X_MAX 11 24 | #define Y_MAX 11 25 | 26 | #define GAME_DELAY_PONG 80 // in ms 27 | #define BALL_DELAY_MAX 350 // in ms 28 | #define BALL_DELAY_MIN 50 // in ms 29 | #define BALL_DELAY_STEP 5 // in ms 30 | 31 | #define PLAYER_AMOUNT 2 32 | #define PLAYER_1 0 33 | #define PLAYER_2 1 34 | 35 | #define PADDLE_WIDTH 3 36 | 37 | #define PADDLE_MOVE_NONE 0 38 | #define PADDLE_MOVE_UP 1 39 | #define PADDLE_MOVE_DOWN 2 40 | 41 | #define LED_TYPE_OFF 1 42 | #define LED_TYPE_PADDLE 2 43 | #define LED_TYPE_BALL 3 44 | #define LED_TYPE_BALL_RED 4 45 | 46 | #define GAME_STATE_RUNNING 1 47 | #define GAME_STATE_END 2 48 | #define GAME_STATE_INIT 3 49 | 50 | class Pong{ 51 | 52 | struct Coords { 53 | uint8_t x; 54 | uint8_t y; 55 | }; 56 | 57 | public: 58 | Pong(); 59 | Pong(LEDMatrix *myledmatrix, UDPLogger *mylogger); 60 | void loopCycle(); 61 | void initGame(uint8_t numBots); 62 | void ctrlUp(uint8_t playerid); 63 | void ctrlDown(uint8_t playerid); 64 | void ctrlNone(uint8_t playerid); 65 | 66 | private: 67 | LEDMatrix *_ledmatrix; 68 | UDPLogger *_logger; 69 | uint8_t _gameState; 70 | uint8_t _numBots; 71 | uint8_t _playerMovement[PLAYER_AMOUNT]; 72 | Coords _paddles[PLAYER_AMOUNT][PADDLE_WIDTH]; 73 | Coords _ball; 74 | Coords _ball_old; 75 | int _ballMovement[2]; 76 | unsigned int _ballDelay; 77 | unsigned long _lastDrawUpdate = 0; 78 | unsigned long _lastBallUpdate = 0; 79 | unsigned long _lastButtonClick = 0; 80 | 81 | 82 | void updateBall(); 83 | void endGame(); 84 | void updateGame(); 85 | uint8_t getPlayerMovement(uint8_t playerId); 86 | void resetLEDs(); 87 | void toggleLed(uint8_t x, uint8_t y, uint8_t type); 88 | }; 89 | 90 | #endif -------------------------------------------------------------------------------- /secrets_example.h: -------------------------------------------------------------------------------- 1 | // WIFI credentials 2 | #define WIFI_SSID "myssid" 3 | #define WIFI_PASS "mypassword" 4 | 5 | // credentials for Access Point 6 | #define AP_SSID "WordclockAP" 7 | #define AP_PASS "appassword" 8 | -------------------------------------------------------------------------------- /snake.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file snake.cpp 3 | * @author techniccontroller (mail[at]techniccontroller.com) 4 | * @brief Class implementation of snake game 5 | * @version 0.1 6 | * @date 2022-03-05 7 | * 8 | * @copyright Copyright (c) 2022 9 | * 10 | * main code from https://elektro.turanis.de/html/prj099/index.html 11 | * 12 | */ 13 | #include "snake.h" 14 | 15 | /** 16 | * @brief Construct a new Snake:: Snake object 17 | * 18 | */ 19 | Snake::Snake(){ 20 | 21 | } 22 | 23 | /** 24 | * @brief Construct a new Snake:: Snake object 25 | * 26 | * @param myledmatrix pointer to LEDMatrix object, need to provide gridAddPixel(x, y, col), gridFlush() 27 | * @param mylogger pointer to UDPLogger object, need to provide a function logString(message) 28 | */ 29 | Snake::Snake(LEDMatrix *myledmatrix, UDPLogger *mylogger){ 30 | _logger = mylogger; 31 | _ledmatrix = myledmatrix; 32 | _gameState = GAME_STATE_END; 33 | } 34 | 35 | /** 36 | * @brief Run main loop for one cycle 37 | * 38 | */ 39 | void Snake::loopCycle() 40 | { 41 | switch(_gameState) 42 | { 43 | case GAME_STATE_INIT: 44 | initGame(); 45 | break; 46 | case GAME_STATE_RUNNING: 47 | updateGame(); 48 | break; 49 | case GAME_STATE_END: 50 | break; 51 | } 52 | } 53 | 54 | /** 55 | * @brief Trigger control: UP 56 | * 57 | */ 58 | void Snake::ctrlUp(){ 59 | if (millis() > _lastButtonClick + DEBOUNCE_TIME_SNAKE && _gameState == GAME_STATE_RUNNING) { 60 | (*_logger).logString("Snake: UP"); 61 | _userDirection = DIRECTION_DOWN; // need to swap direction as field is rotated 180deg 62 | _lastButtonClick = millis(); 63 | } 64 | } 65 | 66 | /** 67 | * @brief Trigger control: DOWN 68 | * 69 | */ 70 | void Snake::ctrlDown(){ 71 | if (millis() > _lastButtonClick + DEBOUNCE_TIME_SNAKE && _gameState == GAME_STATE_RUNNING) { 72 | (*_logger).logString("Snake: DOWN"); 73 | _userDirection = DIRECTION_UP; // need to swap direction as field is rotated 180deg 74 | _lastButtonClick = millis(); 75 | } 76 | } 77 | 78 | /** 79 | * @brief Trigger control: RIGHT 80 | * 81 | */ 82 | void Snake::ctrlRight(){ 83 | if (millis() > _lastButtonClick + DEBOUNCE_TIME_SNAKE && _gameState == GAME_STATE_RUNNING) { 84 | (*_logger).logString("Snake: RIGHT"); 85 | _userDirection = DIRECTION_LEFT; // need to swap direction as field is rotated 180deg 86 | _lastButtonClick = millis(); 87 | } 88 | } 89 | 90 | /** 91 | * @brief Trigger control: LEFT 92 | * 93 | */ 94 | void Snake::ctrlLeft(){ 95 | if (millis() > _lastButtonClick + DEBOUNCE_TIME_SNAKE && _gameState == GAME_STATE_RUNNING) { 96 | (*_logger).logString("Snake: LEFT"); 97 | _userDirection = DIRECTION_RIGHT; // need to swap direction as field is rotated 180deg 98 | _lastButtonClick = millis(); 99 | } 100 | } 101 | 102 | /** 103 | * @brief Clear the led matrix (turn all leds off) 104 | * 105 | */ 106 | void Snake::resetLEDs() 107 | { 108 | (*_ledmatrix).gridFlush(); 109 | } 110 | 111 | /** 112 | * @brief Initialize a new game 113 | * 114 | */ 115 | void Snake::initGame() 116 | { 117 | (*_logger).logString("Snake: init"); 118 | resetLEDs(); 119 | _head.x = 0; 120 | _head.y = 0; 121 | _food.x = -1; 122 | _food.y = -1; 123 | _wormLength = MIN_TAIL_LENGTH; 124 | _userDirection = DIRECTION_LEFT; 125 | _lastButtonClick = millis(); 126 | 127 | for(int i=0; i GAME_DELAY_SNAKE) { 142 | (*_logger).logString("Snake: update game"); 143 | toggleLed(_tail[_wormLength-1].x, _tail[_wormLength-1].y, LED_TYPE_EMPTY); 144 | switch(_userDirection) { 145 | case DIRECTION_RIGHT: 146 | if (_head.x > 0) { 147 | _head.x--; 148 | } 149 | break; 150 | case DIRECTION_LEFT: 151 | if (_head.x < X_MAX-1) { 152 | _head.x++; 153 | } 154 | break; 155 | case DIRECTION_DOWN: 156 | if (_head.y > 0) { 157 | _head.y--; 158 | } 159 | break; 160 | case DIRECTION_UP: 161 | if (_head.y < Y_MAX-1) { 162 | _head.y++; 163 | } 164 | break; 165 | } 166 | 167 | if (isCollision() == true) { 168 | endGame(); 169 | return; 170 | } 171 | 172 | updateTail(); 173 | 174 | if (_head.x == _food.x && _head.y == _food.y) { 175 | if (_wormLength < MAX_TAIL_LENGTH) { 176 | _wormLength++; 177 | } 178 | updateFood(); 179 | } 180 | 181 | _lastDrawUpdate = millis(); 182 | } 183 | } 184 | 185 | /** 186 | * @brief Game over, draw _head red 187 | * 188 | */ 189 | void Snake::endGame() 190 | { 191 | _gameState = GAME_STATE_END; 192 | toggleLed(_head.x, _head.y, LED_TYPE_BLOOD); 193 | } 194 | 195 | /** 196 | * @brief Update _tail led positions 197 | * 198 | */ 199 | void Snake::updateTail() 200 | { 201 | for(unsigned int i=_wormLength-1; i>0; i--) { 202 | _tail[i].x = _tail[i-1].x; 203 | _tail[i].y = _tail[i-1].y; 204 | } 205 | _tail[0].x = _head.x; 206 | _tail[0].y = _head.y; 207 | 208 | for(unsigned int i=0; i<_wormLength; i++) { 209 | if (_tail[i].x > -1) { 210 | toggleLed(_tail[i].x, _tail[i].y, LED_TYPE_SNAKE); 211 | } 212 | } 213 | } 214 | 215 | /** 216 | * @brief Update _food position (generate new one if found) 217 | * 218 | */ 219 | void Snake::updateFood() 220 | { 221 | bool found = true; 222 | do { 223 | found = true; 224 | _food.x = random(0, X_MAX); 225 | _food.y = random(0, Y_MAX); 226 | for(unsigned int i=0; i<_wormLength; i++) { 227 | if (_tail[i].x == _food.x && _tail[i].y == _food.y) { 228 | found = false; 229 | } 230 | } 231 | } while(found == false); 232 | toggleLed(_food.x, _food.y, LED_TYPE_FOOD); 233 | } 234 | 235 | /** 236 | * @brief Check for collisison between snake and border or itself 237 | * 238 | * @return true 239 | * @return false 240 | */ 241 | bool Snake::isCollision() 242 | { 243 | if (_head.x < 0 || _head.x >= X_MAX) { 244 | return true; 245 | } 246 | if (_head.y < 0 || _head.y >= Y_MAX) { 247 | return true; 248 | } 249 | for(unsigned int i=1; i<_wormLength; i++) { 250 | if (_tail[i].x == _head.x && _tail[i].y == _head.y) { 251 | return true; 252 | } 253 | } 254 | return false; 255 | } 256 | 257 | /** 258 | * @brief Turn on LED on matrix 259 | * 260 | * @param x x position of led 261 | * @param y y position of led 262 | * @param type type of pixel {SNAKE, OFF, FOOD, BLOOD} 263 | */ 264 | void Snake::toggleLed(uint8_t x, uint8_t y, uint8_t type) 265 | { 266 | uint32_t color = LEDMatrix::Color24bit(0, 0, 0); 267 | 268 | switch(type) { 269 | case LED_TYPE_SNAKE: 270 | color = LEDMatrix::Color24bit(0, 100, 100); 271 | break; 272 | case LED_TYPE_EMPTY: 273 | color = LEDMatrix::Color24bit(0, 0, 0); 274 | break; 275 | case LED_TYPE_FOOD: 276 | color = LEDMatrix::Color24bit(0, 150, 0); 277 | break; 278 | case LED_TYPE_BLOOD: 279 | color = LEDMatrix::Color24bit(150, 0, 0); 280 | break; 281 | } 282 | 283 | (*_ledmatrix).gridAddPixel(x, y, color); 284 | } -------------------------------------------------------------------------------- /snake.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file snake.h 3 | * @author techniccontroller (mail[at]techniccontroller.com) 4 | * @brief Class declaration of snake game 5 | * @version 0.1 6 | * @date 2022-03-05 7 | * 8 | * @copyright Copyright (c) 2022 9 | * 10 | * main code from https://elektro.turanis.de/html/prj099/index.html 11 | * 12 | */ 13 | #ifndef snake_h 14 | #define snake_h 15 | 16 | #include 17 | #include "ledmatrix.h" 18 | #include "udplogger.h" 19 | 20 | #define DEBOUNCE_TIME_SNAKE 300 // in ms 21 | 22 | #define X_MAX 11 23 | #define Y_MAX 11 24 | 25 | #define GAME_DELAY_SNAKE 400 // in ms 26 | 27 | #define LED_TYPE_SNAKE 1 28 | #define LED_TYPE_EMPTY 2 29 | #define LED_TYPE_FOOD 3 30 | #define LED_TYPE_BLOOD 4 31 | 32 | #define DIRECTION_NONE 0 33 | #define DIRECTION_UP 1 34 | #define DIRECTION_DOWN 2 35 | #define DIRECTION_LEFT 3 36 | #define DIRECTION_RIGHT 4 37 | 38 | #define GAME_STATE_RUNNING 1 39 | #define GAME_STATE_END 2 40 | #define GAME_STATE_INIT 3 41 | 42 | #define MAX_TAIL_LENGTH X_MAX * Y_MAX 43 | #define MIN_TAIL_LENGTH 3 44 | 45 | class Snake{ 46 | 47 | struct Coords { 48 | int x; 49 | int y; 50 | }; 51 | 52 | public: 53 | Snake(); 54 | Snake(LEDMatrix *myledmatrix, UDPLogger *mylogger); 55 | void loopCycle(); 56 | void initGame(); 57 | void ctrlUp(); 58 | void ctrlDown(); 59 | void ctrlLeft(); 60 | void ctrlRight(); 61 | 62 | private: 63 | LEDMatrix *_ledmatrix; 64 | UDPLogger *_logger; 65 | uint8_t _userDirection; 66 | uint8_t _gameState; 67 | Coords _head; 68 | Coords _tail[MAX_TAIL_LENGTH]; 69 | Coords _food; 70 | unsigned long _lastDrawUpdate = 0; 71 | unsigned long _lastButtonClick; 72 | unsigned int _wormLength = 0; 73 | 74 | void resetLEDs(); 75 | void updateGame(); 76 | void endGame(); 77 | void updateTail(); 78 | void updateFood(); 79 | bool isCollision(); 80 | void toggleLed(uint8_t x, uint8_t y, uint8_t type); 81 | 82 | }; 83 | 84 | #endif -------------------------------------------------------------------------------- /tetris.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file tetris.cpp 3 | * @author techniccontroller (mail[at]techniccontroller.com) 4 | * @brief Class implementation for tetris game 5 | * @version 0.1 6 | * @date 2022-03-05 7 | * 8 | * @copyright Copyright (c) 2022 9 | * 10 | * main tetris code originally written by Klaas De Craemer, Ing. David Hrbaty 11 | * 12 | */ 13 | #include "tetris.h" 14 | 15 | Tetris::Tetris(){ 16 | } 17 | 18 | /** 19 | * @brief Construct a new Tetris:: Tetris object 20 | * 21 | * @param myledmatrix pointer to LEDMatrix object, need to provide gridAddPixel(x, y, col), drawOnMatrix(), gridFlush() and printNumber(x,y,n,col) 22 | * @param mylogger pointer to UDPLogger object, need to provide a function logString(message) 23 | */ 24 | Tetris::Tetris(LEDMatrix *myledmatrix, UDPLogger *mylogger){ 25 | _logger = mylogger; 26 | _ledmatrix = myledmatrix; 27 | _gameStatet = GAME_STATE_READYt; 28 | } 29 | 30 | /** 31 | * @brief Run main loop for one cycle 32 | * 33 | */ 34 | void Tetris::loopCycle(){ 35 | switch (_gameStatet) { 36 | case GAME_STATE_READYt: 37 | 38 | break; 39 | case GAME_STATE_INITt: 40 | tetrisInit(); 41 | 42 | break; 43 | case GAME_STATE_RUNNINGt: 44 | //If brick is still "on the loose", then move it down by one 45 | if (_activeBrick.enabled) { 46 | // move faster down when allow drop 47 | if (_allowdrop) { 48 | if (millis() > _dropTime + 50) { 49 | _dropTime = millis(); 50 | shiftActiveBrick(DIR_DOWN); 51 | printField(); 52 | } 53 | } 54 | 55 | // move down with regular speed 56 | if ((millis() - _prevUpdateTime) > (_brickSpeed * _speedtetris / 100)) { 57 | _prevUpdateTime = millis(); 58 | shiftActiveBrick(DIR_DOWN); 59 | printField(); 60 | } 61 | } 62 | else { 63 | _allowdrop = false; 64 | //Active brick has "crashed", check for full lines 65 | //and create new brick at top of field 66 | checkFullLines(); 67 | newActiveBrick(); 68 | _prevUpdateTime = millis();//Reset update time to avoid brick dropping two spaces 69 | } 70 | break; 71 | case GAME_STATE_PAUSEDt: 72 | 73 | break; 74 | case GAME_STATE_ENDt: 75 | // at game end show all bricks on field in red color for 1.5 seconds, then show score 76 | if (_tetrisGameOver == true) { 77 | _tetrisGameOver = false; 78 | (*_logger).logString("Tetris: end"); 79 | everythingRed(); 80 | _tetrisshowscoreTime = millis(); 81 | } 82 | 83 | if (millis() > (_tetrisshowscoreTime + RED_END_TIME)) { 84 | resetLEDs(); 85 | _score = _nbRowsTotal; 86 | showscore(); 87 | _gameStatet = GAME_STATE_READYt; 88 | } 89 | break; 90 | } 91 | } 92 | 93 | /** 94 | * @brief Trigger control: START (& restart) 95 | * 96 | */ 97 | void Tetris::ctrlStart() { 98 | if (millis() > _lastButtonClick + DEBOUNCE_TIME_TETRIS) 99 | { 100 | _lastButtonClick = millis(); 101 | _gameStatet = GAME_STATE_INITt; 102 | } 103 | } 104 | 105 | /** 106 | * @brief Trigger control: PAUSE/PLAY 107 | * 108 | */ 109 | void Tetris::ctrlPlayPause() { 110 | if (millis() > _lastButtonClick + DEBOUNCE_TIME_TETRIS) 111 | { 112 | _lastButtonClick = millis(); 113 | if (_gameStatet == GAME_STATE_PAUSEDt) { 114 | (*_logger).logString("Tetris: continue"); 115 | 116 | _gameStatet = GAME_STATE_RUNNINGt; 117 | 118 | } else if (_gameStatet == GAME_STATE_RUNNINGt) { 119 | (*_logger).logString("Tetris: pause"); 120 | 121 | _gameStatet = GAME_STATE_PAUSEDt; 122 | } 123 | } 124 | } 125 | 126 | /** 127 | * @brief Trigger control: RIGHT 128 | * 129 | */ 130 | void Tetris::ctrlRight() { 131 | if (millis() > _lastButtonClick + DEBOUNCE_TIME_TETRIS && _gameStatet == GAME_STATE_RUNNINGt) 132 | { 133 | _lastButtonClick = millis(); 134 | shiftActiveBrick(DIR_RIGHT); 135 | printField(); 136 | } 137 | } 138 | 139 | /** 140 | * @brief Trigger control: LEFT 141 | * 142 | */ 143 | void Tetris::ctrlLeft() { 144 | if (millis() > _lastButtonClick + DEBOUNCE_TIME_TETRIS && _gameStatet == GAME_STATE_RUNNINGt) 145 | { 146 | _lastButtonClick = millis(); 147 | shiftActiveBrick(DIR_LEFT); 148 | printField(); 149 | } 150 | } 151 | 152 | /** 153 | * @brief Trigger control: UP (rotate) 154 | * 155 | */ 156 | void Tetris::ctrlUp() { 157 | if (millis() > _lastButtonClick + DEBOUNCE_TIME_TETRIS && _gameStatet == GAME_STATE_RUNNINGt) 158 | { 159 | _lastButtonClick = millis(); 160 | rotateActiveBrick(); 161 | printField(); 162 | } 163 | } 164 | 165 | /** 166 | * @brief Trigger control: DOWN (drop) 167 | * 168 | */ 169 | void Tetris::ctrlDown() { 170 | // longer debounce time, to prevent immediate drop 171 | if (millis() > _lastButtonClickr + DEBOUNCE_TIME_TETRIS*5 && _gameStatet == GAME_STATE_RUNNINGt) 172 | { 173 | _allowdrop = true; 174 | _lastButtonClickr = millis(); 175 | } 176 | } 177 | 178 | /** 179 | * @brief Set game speed 180 | * 181 | * @param i new speed value (0 - 15) 182 | */ 183 | void Tetris::setSpeed(uint8_t i) { 184 | if(i > 15) i = 15; 185 | (*_logger).logString("setSpeed: " + String(i)); 186 | _speedtetris = -10 * i + 150; 187 | } 188 | 189 | /** 190 | * @brief Clear the led matrix (turn all leds off) 191 | * 192 | */ 193 | void Tetris::resetLEDs() 194 | { 195 | (*_ledmatrix).gridFlush(); 196 | (*_ledmatrix).drawOnMatrixInstant(); 197 | } 198 | 199 | /** 200 | * @brief Initialize the tetris game 201 | * 202 | */ 203 | void Tetris::tetrisInit() { 204 | (*_logger).logString("Tetris: init"); 205 | 206 | clearField(); 207 | _brickSpeed = INIT_SPEED; 208 | _nbRowsThisLevel = 0; 209 | _nbRowsTotal = 0; 210 | _tetrisGameOver = false; 211 | 212 | newActiveBrick(); 213 | _prevUpdateTime = millis(); 214 | 215 | _gameStatet = GAME_STATE_RUNNINGt; 216 | } 217 | 218 | /** 219 | * @brief Draw current field representation to led matrix 220 | * 221 | */ 222 | void Tetris::printField() { 223 | int x, y; 224 | for (x = 0; x < WIDTH; x++) { 225 | for (y = 0; y < HEIGHT; y++) { 226 | uint8_t activeBrickPix = 0; 227 | if (_activeBrick.enabled) { //Only draw brick if it is enabled 228 | //Now check if brick is "in view" 229 | if ((x >= _activeBrick.xpos) && (x < (_activeBrick.xpos + (_activeBrick.siz))) 230 | && (y >= _activeBrick.ypos) && (y < (_activeBrick.ypos + (_activeBrick.siz)))) { 231 | activeBrickPix = (_activeBrick.pix)[x - _activeBrick.xpos][y - _activeBrick.ypos]; 232 | } 233 | } 234 | if (_field.pix[x][y] == 1) { 235 | (*_ledmatrix).gridAddPixel(x, y, _field.color[x][y]); 236 | } else if (activeBrickPix == 1) { 237 | (*_ledmatrix).gridAddPixel(x, y, _activeBrick.col); 238 | } else { 239 | (*_ledmatrix).gridAddPixel(x, y, 0x000000); 240 | } 241 | } 242 | } 243 | (*_ledmatrix).drawOnMatrixInstant(); 244 | } 245 | 246 | 247 | /* *** Game functions *** */ 248 | /** 249 | * @brief Spawn new (random) brick 250 | * 251 | */ 252 | void Tetris::newActiveBrick() { 253 | uint8_t selectedBrick = 0; 254 | static uint8_t lastselectedBrick = 0; 255 | 256 | // choose random next brick, but not the same as before 257 | do { 258 | selectedBrick = random(7); 259 | } 260 | while (lastselectedBrick == selectedBrick); 261 | 262 | // Save selected brick for next round 263 | lastselectedBrick = selectedBrick; 264 | 265 | // every brick has its color, select corresponding color 266 | uint32_t selectedCol = _brickLib[selectedBrick].col; 267 | // Set properties of brick 268 | _activeBrick.siz = _brickLib[selectedBrick].siz; 269 | _activeBrick.yOffset = _brickLib[selectedBrick].yOffset; 270 | _activeBrick.xpos = WIDTH / 2 - _activeBrick.siz / 2; 271 | _activeBrick.ypos = BRICKOFFSET - _activeBrick.yOffset; 272 | _activeBrick.enabled = true; 273 | 274 | // Set color of brick 275 | _activeBrick.col = selectedCol; 276 | // _activeBrick.color = _colorLib[1]; 277 | 278 | // Copy pix array of selected Brick 279 | uint8_t x, y; 280 | for (y = 0; y < MAX_BRICK_SIZE; y++) { 281 | for (x = 0; x < MAX_BRICK_SIZE; x++) { 282 | _activeBrick.pix[x][y] = (_brickLib[selectedBrick]).pix[x][y]; 283 | } 284 | } 285 | 286 | // Check collision, if already, then game is over 287 | if (checkFieldCollision(&_activeBrick)) { 288 | _tetrisGameOver = true; 289 | _gameStatet = GAME_STATE_ENDt; 290 | 291 | } 292 | } 293 | 294 | /** 295 | * @brief Check collision between bricks in the field and the specified brick 296 | * 297 | * @param brick brick to be checked for collision 298 | * @return boolean true if collision occured 299 | */ 300 | boolean Tetris::checkFieldCollision(struct Brick * brick) { 301 | uint8_t bx, by; 302 | uint8_t fx, fy; 303 | for (by = 0; by < MAX_BRICK_SIZE; by++) { 304 | for (bx = 0; bx < MAX_BRICK_SIZE; bx++) { 305 | fx = (*brick).xpos + bx; 306 | fy = (*brick).ypos + by; 307 | if (( (*brick).pix[bx][by] == 1) 308 | && ( _field.pix[fx][fy] == 1)) { 309 | return true; 310 | } 311 | } 312 | } 313 | return false; 314 | } 315 | 316 | /** 317 | * @brief Check collision between specified brick and all sides of the playing field 318 | * 319 | * @param brick brick to be checked for collision 320 | * @return boolean true if collision occured 321 | */ 322 | boolean Tetris::checkSidesCollision(struct Brick * brick) { 323 | //Check vertical collision with sides of field 324 | uint8_t bx, by; 325 | int8_t fx; 326 | for (by = 0; by < MAX_BRICK_SIZE; by++) { 327 | for (bx = 0; bx < MAX_BRICK_SIZE; bx++) { 328 | if ( (*brick).pix[bx][by] == 1) { 329 | fx = (*brick).xpos + bx;//Determine actual position in the field of the current pix of the brick 330 | if (fx < 0 || fx >= WIDTH) { 331 | return true; 332 | } 333 | } 334 | } 335 | } 336 | return false; 337 | } 338 | 339 | /** 340 | * @brief Rotate current active brick 341 | * 342 | */ 343 | void Tetris::rotateActiveBrick() { 344 | //Copy active brick pix array to temporary pix array 345 | uint8_t x, y; 346 | Brick tmpBrick; 347 | for (y = 0; y < MAX_BRICK_SIZE; y++) { 348 | for (x = 0; x < MAX_BRICK_SIZE; x++) { 349 | tmpBrick.pix[x][y] = _activeBrick.pix[x][y]; 350 | } 351 | } 352 | tmpBrick.xpos = _activeBrick.xpos; 353 | tmpBrick.ypos = _activeBrick.ypos; 354 | tmpBrick.siz = _activeBrick.siz; 355 | 356 | //Depending on size of the active brick, we will rotate differently 357 | if (_activeBrick.siz == 3) { 358 | //Perform rotation around center pix 359 | tmpBrick.pix[0][0] = _activeBrick.pix[0][2]; 360 | tmpBrick.pix[0][1] = _activeBrick.pix[1][2]; 361 | tmpBrick.pix[0][2] = _activeBrick.pix[2][2]; 362 | tmpBrick.pix[1][0] = _activeBrick.pix[0][1]; 363 | tmpBrick.pix[1][1] = _activeBrick.pix[1][1]; 364 | tmpBrick.pix[1][2] = _activeBrick.pix[2][1]; 365 | tmpBrick.pix[2][0] = _activeBrick.pix[0][0]; 366 | tmpBrick.pix[2][1] = _activeBrick.pix[1][0]; 367 | tmpBrick.pix[2][2] = _activeBrick.pix[2][0]; 368 | //Keep other parts of temporary block clear 369 | tmpBrick.pix[0][3] = 0; 370 | tmpBrick.pix[1][3] = 0; 371 | tmpBrick.pix[2][3] = 0; 372 | tmpBrick.pix[3][3] = 0; 373 | tmpBrick.pix[3][2] = 0; 374 | tmpBrick.pix[3][1] = 0; 375 | tmpBrick.pix[3][0] = 0; 376 | 377 | } else if (_activeBrick.siz == 4) { 378 | //Perform rotation around center "cross" 379 | tmpBrick.pix[0][0] = _activeBrick.pix[0][3]; 380 | tmpBrick.pix[0][1] = _activeBrick.pix[1][3]; 381 | tmpBrick.pix[0][2] = _activeBrick.pix[2][3]; 382 | tmpBrick.pix[0][3] = _activeBrick.pix[3][3]; 383 | tmpBrick.pix[1][0] = _activeBrick.pix[0][2]; 384 | tmpBrick.pix[1][1] = _activeBrick.pix[1][2]; 385 | tmpBrick.pix[1][2] = _activeBrick.pix[2][2]; 386 | tmpBrick.pix[1][3] = _activeBrick.pix[3][2]; 387 | tmpBrick.pix[2][0] = _activeBrick.pix[0][1]; 388 | tmpBrick.pix[2][1] = _activeBrick.pix[1][1]; 389 | tmpBrick.pix[2][2] = _activeBrick.pix[2][1]; 390 | tmpBrick.pix[2][3] = _activeBrick.pix[3][1]; 391 | tmpBrick.pix[3][0] = _activeBrick.pix[0][0]; 392 | tmpBrick.pix[3][1] = _activeBrick.pix[1][0]; 393 | tmpBrick.pix[3][2] = _activeBrick.pix[2][0]; 394 | tmpBrick.pix[3][3] = _activeBrick.pix[3][0]; 395 | } else { 396 | (*_logger).logString("Tetris: Brick size error"); 397 | } 398 | 399 | // Now validate by checking collision. 400 | // Collision possibilities: 401 | // - Brick now sticks outside field 402 | // - Brick now sticks inside fixed bricks of field 403 | // In case of collision, we just discard the rotated temporary brick 404 | if ((!checkSidesCollision(&tmpBrick)) && (!checkFieldCollision(&tmpBrick))) { 405 | //Copy temporary brick pix array to active pix array 406 | for (y = 0; y < MAX_BRICK_SIZE; y++) { 407 | for (x = 0; x < MAX_BRICK_SIZE; x++) { 408 | _activeBrick.pix[x][y] = tmpBrick.pix[x][y]; 409 | } 410 | } 411 | } 412 | } 413 | 414 | /** 415 | * @brief Shift brick left/right/down by one if possible 416 | * 417 | * @param dir direction to be shifted 418 | */ 419 | void Tetris::shiftActiveBrick(int dir) { 420 | // Change position of active brick (no copy to temporary needed) 421 | if (dir == DIR_LEFT) { 422 | _activeBrick.xpos--; 423 | } else if (dir == DIR_RIGHT) { 424 | _activeBrick.xpos++; 425 | } else if (dir == DIR_DOWN) { 426 | _activeBrick.ypos++; 427 | } 428 | 429 | // Check position of active brick 430 | // Two possibilities when collision is detected: 431 | // - Direction was LEFT/RIGHT, just revert position back 432 | // - Direction was DOWN, revert position and fix block to field on collision 433 | // When no collision, keep _activeBrick coordinates 434 | if ((checkSidesCollision(&_activeBrick)) || (checkFieldCollision(&_activeBrick))) { 435 | if (dir == DIR_LEFT) { 436 | _activeBrick.xpos++; 437 | } else if (dir == DIR_RIGHT) { 438 | _activeBrick.xpos--; 439 | } else if (dir == DIR_DOWN) { 440 | _activeBrick.ypos--;// Go back up one 441 | addActiveBrickToField(); 442 | _activeBrick.enabled = false;// Disable brick, it is no longer moving 443 | } 444 | } 445 | } 446 | 447 | /** 448 | * @brief Copy active pixels to field, including color 449 | * 450 | */ 451 | void Tetris::addActiveBrickToField() { 452 | uint8_t bx, by; 453 | int8_t fx, fy; 454 | for (by = 0; by < MAX_BRICK_SIZE; by++) { 455 | for (bx = 0; bx < MAX_BRICK_SIZE; bx++) { 456 | fx = _activeBrick.xpos + bx; 457 | fy = _activeBrick.ypos + by; 458 | 459 | if (fx >= 0 && fy >= 0 && fx < WIDTH && fy < HEIGHT && _activeBrick.pix[bx][by]) { // Check if inside playing field 460 | // _field.pix[fx][fy] = _field.pix[fx][fy] || _activeBrick.pix[bx][by]; 461 | _field.pix[fx][fy] = _activeBrick.pix[bx][by]; 462 | _field.color[fx][fy] = _activeBrick.col; 463 | } 464 | } 465 | } 466 | } 467 | 468 | /** 469 | * @brief Move all pix from the field above startRow down by one. startRow is overwritten 470 | * 471 | * @param startRow 472 | */ 473 | void Tetris::moveFieldDownOne(uint8_t startRow) { 474 | if (startRow == 0) { // Topmost row has nothing on top to move... 475 | return; 476 | } 477 | uint8_t x, y; 478 | for (y = startRow - 1; y > 0; y--) { 479 | for (x = 0; x < WIDTH; x++) { 480 | _field.pix[x][y + 1] = _field.pix[x][y]; 481 | _field.color[x][y + 1] = _field.color[x][y]; 482 | } 483 | } 484 | } 485 | 486 | /** 487 | * @brief Check if a line is complete 488 | * 489 | */ 490 | void Tetris::checkFullLines() { 491 | int x, y; 492 | int minY = 0; 493 | for (y = (HEIGHT - 1); y >= minY; y--) { 494 | uint8_t rowSum = 0; 495 | for (x = 0; x < WIDTH; x++) { 496 | rowSum = rowSum + (_field.pix[x][y]); 497 | } 498 | if (rowSum >= WIDTH) { 499 | // Found full row, animate its removal 500 | _activeBrick.enabled = false; 501 | 502 | for (x = 0; x < WIDTH; x++) { 503 | _field.pix[x][y] = 0; 504 | printField(); 505 | delay(100); 506 | } 507 | // Move all upper rows down by one 508 | moveFieldDownOne(y); 509 | y++; minY++; 510 | printField(); 511 | delay(100); 512 | 513 | 514 | _nbRowsThisLevel++; _nbRowsTotal++; 515 | if (_nbRowsThisLevel >= LEVELUP) { 516 | _nbRowsThisLevel = 0; 517 | _brickSpeed = _brickSpeed - SPEED_STEP; 518 | if (_brickSpeed < 200) { 519 | _brickSpeed = 200; 520 | } 521 | } 522 | } 523 | } 524 | } 525 | 526 | /** 527 | * @brief Clear field 528 | * 529 | */ 530 | void Tetris::clearField() { 531 | uint8_t x, y; 532 | for (y = 0; y < HEIGHT; y++) { 533 | for (x = 0; x < WIDTH; x++) { 534 | _field.pix[x][y] = 0; 535 | _field.color[x][y] = 0; 536 | } 537 | } 538 | for (x = 0; x < WIDTH; x++) { //This last row is invisible to the player and only used for the collision detection routine 539 | _field.pix[x][HEIGHT] = 1; 540 | } 541 | } 542 | 543 | /** 544 | * @brief Color all bricks on the field red 545 | * 546 | */ 547 | void Tetris::everythingRed() { 548 | int x, y; 549 | for (x = 0; x < WIDTH; x++) { 550 | for (y = 0; y < HEIGHT; y++) { 551 | uint8_t activeBrickPix = 0; 552 | if (_activeBrick.enabled) { //Only draw brick if it is enabled 553 | //Now check if brick is "in view" 554 | if ((x >= _activeBrick.xpos) && (x < (_activeBrick.xpos + (_activeBrick.siz))) 555 | && (y >= _activeBrick.ypos) && (y < (_activeBrick.ypos + (_activeBrick.siz)))) { 556 | activeBrickPix = (_activeBrick.pix)[x - _activeBrick.xpos][y - _activeBrick.ypos]; 557 | } 558 | } 559 | if (_field.pix[x][y] == 1) { 560 | (*_ledmatrix).gridAddPixel(x, y, RED); 561 | } else if (activeBrickPix == 1) { 562 | (*_ledmatrix).gridAddPixel(x, y, RED); 563 | } else { 564 | (*_ledmatrix).gridAddPixel(x, y, 0x000000); 565 | } 566 | } 567 | } 568 | (*_ledmatrix).drawOnMatrixInstant(); 569 | } 570 | 571 | /** 572 | * @brief Draw score to led matrix 573 | * 574 | */ 575 | void Tetris::showscore() { 576 | uint32_t color = LEDMatrix::Color24bit(255, 170, 0); 577 | (*_ledmatrix).gridFlush(); 578 | if(_score > 9){ 579 | (*_ledmatrix).printNumber(2, 3, _score/10, color); 580 | (*_ledmatrix).printNumber(6, 3, _score%10, color); 581 | }else{ 582 | (*_ledmatrix).printNumber(4, 3, _score, color); 583 | } 584 | (*_ledmatrix).drawOnMatrixInstant(); 585 | } -------------------------------------------------------------------------------- /tetris.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file tetris.h 3 | * @author techniccontroller (mail[at]techniccontroller.com) 4 | * @brief Class definition for tetris game 5 | * @version 0.1 6 | * @date 2022-03-05 7 | * 8 | * @copyright Copyright (c) 2022 9 | * 10 | * main tetris code originally written by Klaas De Craemer, Ing. David Hrbaty 11 | * 12 | */ 13 | #ifndef tetris_h 14 | #define tetris_h 15 | 16 | #include 17 | #include "ledmatrix.h" 18 | #include "udplogger.h" 19 | 20 | #define DEBOUNCE_TIME_TETRIS 100 21 | #define RED_END_TIME 1500 22 | #define GAME_STATE_RUNNINGt 1 23 | #define GAME_STATE_ENDt 2 24 | #define GAME_STATE_INITt 3 25 | #define GAME_STATE_PAUSEDt 4 26 | #define GAME_STATE_READYt 5 27 | 28 | //common 29 | #define DIR_UP 1 30 | #define DIR_DOWN 2 31 | #define DIR_LEFT 3 32 | #define DIR_RIGHT 4 33 | //Maximum size of bricks. Individual bricks can still be smaller (eg 3x3) 34 | #define GREEN 0x008000 35 | #define RED 0xFF0000 36 | #define BLUE 0x0000FF 37 | #define YELLOW 0xFFFF00 38 | #define CHOCOLATE 0xD2691E 39 | #define PURPLE 0xFF00FF 40 | #define WHITE 0XFFFFFF 41 | #define AQUA 0x00FFFF 42 | #define HOTPINK 0xFF1493 43 | #define DARKORANGE 0xFF8C00 44 | 45 | #define MAX_BRICK_SIZE 4 46 | #define BRICKOFFSET -1 // Y offset for new bricks 47 | 48 | #define INIT_SPEED 800 // Initial delay in ms between brick drops 49 | #define SPEED_STEP 10 // Factor for speed increase between levels, default 10 50 | #define LEVELUP 4 // Number of rows before levelup, default 5 51 | 52 | #define WIDTH 11 53 | #define HEIGHT 11 54 | 55 | class Tetris{ 56 | 57 | // Playing field 58 | struct Field { 59 | uint8_t pix[WIDTH][HEIGHT + 1]; //Make field one larger so that collision detection with bottom of field can be done in a uniform way 60 | uint32_t color[WIDTH][HEIGHT]; 61 | }; 62 | 63 | 64 | //Structure to represent active brick on screen 65 | struct Brick { 66 | boolean enabled;//Brick is disabled when it has landed 67 | int xpos, ypos; 68 | int yOffset;//Y-offset to use when placing brick at top of field 69 | uint8_t siz; 70 | uint8_t pix[MAX_BRICK_SIZE][MAX_BRICK_SIZE]; 71 | 72 | uint32_t col; 73 | }; 74 | 75 | //Struct to contain the different choices of blocks 76 | struct AbstractBrick { 77 | int yOffset;//Y-offset to use when placing brick at top of field 78 | uint8_t siz; 79 | uint8_t pix[MAX_BRICK_SIZE][MAX_BRICK_SIZE]; 80 | uint32_t col; 81 | }; 82 | 83 | public: 84 | Tetris(); 85 | Tetris(LEDMatrix *myledmatrix, UDPLogger *mylogger); 86 | 87 | void ctrlStart(); 88 | void ctrlPlayPause(); 89 | void ctrlRight(); 90 | void ctrlLeft(); 91 | void ctrlUp(); 92 | void ctrlDown(); 93 | void setSpeed(uint8_t i); 94 | 95 | void loopCycle(); 96 | 97 | private: 98 | void resetLEDs(); 99 | void tetrisInit(); 100 | void printField(); 101 | 102 | /* *** Game functions *** */ 103 | void newActiveBrick(); 104 | boolean checkFieldCollision(struct Brick * brick); 105 | boolean checkSidesCollision(struct Brick * brick); 106 | void rotateActiveBrick(); 107 | void shiftActiveBrick(int dir); 108 | void addActiveBrickToField(); 109 | void moveFieldDownOne(uint8_t startRow); 110 | void checkFullLines(); 111 | 112 | void clearField(); 113 | void everythingRed(); 114 | void showscore(); 115 | 116 | 117 | LEDMatrix *_ledmatrix; 118 | UDPLogger *_logger; 119 | Brick _activeBrick; 120 | Field _field; 121 | 122 | unsigned long _lastButtonClick = 0; 123 | unsigned long _lastButtonClickr = 0; 124 | int _score = 0; 125 | int _gameStatet = GAME_STATE_INITt; 126 | unsigned int _brickSpeed; 127 | unsigned long _nbRowsThisLevel; 128 | unsigned long _nbRowsTotal; 129 | 130 | bool _tetrisGameOver; 131 | 132 | unsigned long _prevUpdateTime = 0; 133 | unsigned long _tetrisshowscoreTime = 0; 134 | unsigned long _dropTime = 0; 135 | unsigned int _speedtetris = 80; 136 | bool _allowdrop; 137 | 138 | // color library 139 | uint32_t _colorLib[10] = {RED, GREEN, BLUE, YELLOW, CHOCOLATE, PURPLE, WHITE, AQUA, HOTPINK, DARKORANGE}; 140 | 141 | // Brick "library" 142 | AbstractBrick _brickLib[7] = { 143 | { 144 | 1,//yoffset when adding brick to field 145 | 4, 146 | { {0, 0, 0, 0}, 147 | {0, 1, 1, 0}, 148 | {0, 1, 1, 0}, 149 | {0, 0, 0, 0} 150 | }, 151 | WHITE 152 | }, 153 | { 154 | 0, 155 | 4, 156 | { {0, 1, 0, 0}, 157 | {0, 1, 0, 0}, 158 | {0, 1, 0, 0}, 159 | {0, 1, 0, 0} 160 | }, 161 | GREEN 162 | }, 163 | { 164 | 1, 165 | 3, 166 | { {0, 0, 0, 0}, 167 | {1, 1, 1, 0}, 168 | {0, 0, 1, 0}, 169 | {0, 0, 0, 0} 170 | }, 171 | BLUE 172 | }, 173 | { 174 | 1, 175 | 3, 176 | { {0, 0, 1, 0}, 177 | {1, 1, 1, 0}, 178 | {0, 0, 0, 0}, 179 | {0, 0, 0, 0} 180 | }, 181 | YELLOW 182 | }, 183 | { 184 | 1, 185 | 3, 186 | { {0, 0, 0, 0}, 187 | {1, 1, 1, 0}, 188 | {0, 1, 0, 0}, 189 | {0, 0, 0, 0} 190 | }, 191 | AQUA 192 | }, 193 | { 194 | 1, 195 | 3, 196 | { {0, 1, 1, 0}, 197 | {1, 1, 0, 0}, 198 | {0, 0, 0, 0}, 199 | {0, 0, 0, 0} 200 | }, 201 | HOTPINK 202 | }, 203 | { 204 | 1, 205 | 3, 206 | { {1, 1, 0, 0}, 207 | {0, 1, 1, 0}, 208 | {0, 0, 0, 0}, 209 | {0, 0, 0, 0} 210 | }, 211 | RED 212 | } 213 | }; 214 | 215 | }; 216 | 217 | #endif -------------------------------------------------------------------------------- /timezonefunctions.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include "ntp_client_plus.h" 3 | #include "udplogger.h" 4 | 5 | int api_offset = 0; 6 | String api_timezone = ""; 7 | float api_lat = 0.0; 8 | float api_lon = 0.0; 9 | 10 | /** 11 | * @brief Request the timezone and other data from the IP-API 12 | * 13 | * @param logger UDPLogger object to log messages 14 | * @return bool true if the api request was successful 15 | */ 16 | bool requestAPIData(UDPLogger &logger) { 17 | WiFiClient client; 18 | HTTPClient http; 19 | bool res = false; 20 | logger.logString("[HTTP] Requesting timezone from IP-API"); 21 | // see API documentation on https://ip-api.com/docs/api:json to see which fields are available 22 | if (http.begin(client, "http://ip-api.com/json/?fields=status,message,country,countryCode,region,regionName,city,zip,lat,lon,timezone,offset,query")) { 23 | int httpCode = http.GET(); 24 | 25 | if (httpCode > 0) { 26 | if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) { 27 | String payload = http.getString(); 28 | 29 | api_timezone = getJsonParameterValue(payload, "timezone", true); 30 | logger.logString("[HTTP] Received timezone: " + api_timezone); 31 | 32 | String offsetString = getJsonParameterValue(payload, "offset", false); 33 | api_offset = offsetString.toInt() / 60; 34 | logger.logString("[HTTP] Received offset (min): " + String(api_offset)); 35 | 36 | String latString = getJsonParameterValue(payload, "lat", false); 37 | api_lat = latString.toFloat(); 38 | logger.logString("[HTTP] Received latitude: " + String(api_lat)); 39 | 40 | String lonString = getJsonParameterValue(payload, "lon", false); 41 | api_lon = lonString.toFloat(); 42 | logger.logString("[HTTP] Received longitude: " + String(api_lon)); 43 | 44 | } 45 | } 46 | else { 47 | Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str()); 48 | res = false; 49 | } 50 | http.end(); // Close connection 51 | } 52 | else { 53 | logger.logString("[HTTP] Unable to connect"); 54 | res = false; 55 | } 56 | return res; 57 | } 58 | 59 | /** 60 | * @brief Get the Json Parameter Value object 61 | * 62 | * @param json 63 | * @param parameter 64 | * @return String 65 | */ 66 | String getJsonParameterValue(String json, String parameter, bool isString) { 67 | String value = ""; 68 | if(isString) { 69 | int index = json.indexOf("\"" + parameter + "\":\""); 70 | if (index != -1) { 71 | int start = index + parameter.length() + 4; 72 | int end = json.indexOf("\"", start); 73 | value = json.substring(start, end); 74 | } 75 | } 76 | else { 77 | int index = json.indexOf("\"" + parameter + "\":"); 78 | if (index != -1) { 79 | int start = index + parameter.length() + 3; 80 | int end = json.indexOf(",", start); 81 | value = json.substring(start, end); 82 | } 83 | } 84 | return value; 85 | } 86 | 87 | /** 88 | * @brief Update the UTC offset from the timezone string obtained from the IP-API 89 | * 90 | * @param logger UDPLogger object to log messages 91 | * @param ntp NTPClientPlus object to set the UTC offset 92 | * @return int 93 | */ 94 | void updateUTCOffsetFromTimezoneAPI(UDPLogger &logger, NTPClientPlus &ntp) { 95 | bool res = requestAPIData(logger); 96 | if (res) { 97 | ntp.setUTCOffset(api_offset); 98 | } 99 | } -------------------------------------------------------------------------------- /udplogger.cpp: -------------------------------------------------------------------------------- 1 | #include "udplogger.h" 2 | 3 | UDPLogger::UDPLogger(){ 4 | 5 | } 6 | 7 | UDPLogger::UDPLogger(IPAddress interfaceAddr, IPAddress multicastAddr, int port){ 8 | _multicastAddr = multicastAddr; 9 | _port = port; 10 | _interfaceAddr = interfaceAddr; 11 | _name = "Log"; 12 | _Udp.beginMulticast(_interfaceAddr, _multicastAddr, _port); 13 | } 14 | 15 | void UDPLogger::setName(String name){ 16 | _name = name; 17 | } 18 | 19 | void UDPLogger::logString(String logmessage){ 20 | // wait 5 milliseconds if last send was less than 5 milliseconds before 21 | if(millis() < (_lastSend + 5)){ 22 | delay(5); 23 | } 24 | logmessage = _name + ": " + logmessage; 25 | Serial.println(logmessage); 26 | _Udp.beginPacketMulticast(_multicastAddr, _port, _interfaceAddr); 27 | logmessage.toCharArray(_packetBuffer, 100); 28 | _Udp.print(_packetBuffer); 29 | _Udp.endPacket(); 30 | _lastSend=millis(); 31 | } 32 | 33 | void UDPLogger::logColor24bit(uint32_t color){ 34 | uint8_t resultRed = color >> 16 & 0xff; 35 | uint8_t resultGreen = color >> 8 & 0xff; 36 | uint8_t resultBlue = color & 0xff; 37 | logString(String(resultRed) + ", " + String(resultGreen) + ", " + String(resultBlue)); 38 | } -------------------------------------------------------------------------------- /udplogger.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file udplogger.h 3 | * @author techniccontroller (mail[at]techniccontroller.com) 4 | * @brief Class for sending logging Strings as multicast messages 5 | * @version 0.1 6 | * @date 2022-03-21 7 | * 8 | * @copyright Copyright (c) 2022 9 | * 10 | */ 11 | 12 | #ifndef udplogger_h 13 | #define udplogger_h 14 | 15 | #include 16 | #include 17 | 18 | 19 | class UDPLogger{ 20 | 21 | public: 22 | UDPLogger(); 23 | UDPLogger(IPAddress interfaceAddr, IPAddress multicastAddr, int port); 24 | void setName(String name); 25 | void logString(String logmessage); 26 | void logColor24bit(uint32_t color); 27 | private: 28 | String _name; 29 | IPAddress _multicastAddr; 30 | IPAddress _interfaceAddr; 31 | int _port; 32 | WiFiUDP _Udp; 33 | char _packetBuffer[100]; 34 | unsigned long _lastSend; 35 | }; 36 | 37 | #endif -------------------------------------------------------------------------------- /wordclockfunctions.ino: -------------------------------------------------------------------------------- 1 | 2 | const String clockStringGerman = "ESPISTAFUNFVIERTELZEHNZWANZIGUVORTECHNICNACHHALBMELFUNFXCONTROLLEREINSEAWZWEIDREITUMVIERSECHSQYACHTSIEBENZWOLFZEHNEUNJUHR"; 3 | 4 | /** 5 | * @brief control the four minute indicator LEDs 6 | * 7 | * @param minutes minutes to be displayed [0 ... 59] 8 | * @param color 24bit color value 9 | */ 10 | void drawMinuteIndicator(uint8_t minutes, uint32_t color){ 11 | //separate LEDs for minutes in an additional row 12 | { 13 | switch (minutes%5) 14 | { 15 | case 0: 16 | break; 17 | 18 | case 1: 19 | ledmatrix.setMinIndicator(0b1000, color); 20 | break; 21 | 22 | case 2: 23 | ledmatrix.setMinIndicator(0b1100, color); 24 | break; 25 | 26 | case 3: 27 | ledmatrix.setMinIndicator(0b1110, color); 28 | break; 29 | 30 | case 4: 31 | ledmatrix.setMinIndicator(0b1111, color); 32 | break; 33 | } 34 | } 35 | } 36 | 37 | /** 38 | * @brief Draw the given sentence to the word clock 39 | * 40 | * @param message sentence to be displayed 41 | * @param color 24bit color value 42 | * @return int: 0 if successful, -1 if sentence not possible to display 43 | */ 44 | int showStringOnClock(String message, uint32_t color){ 45 | String word = ""; 46 | int lastLetterClock = 0; 47 | int positionOfWord = 0; 48 | int index = 0; 49 | 50 | // add space on the end of message for splitting 51 | message = message + " "; 52 | 53 | // empty the targetgrid 54 | ledmatrix.gridFlush(); 55 | 56 | while(true){ 57 | // extract next word from message 58 | word = split(message, ' ', index); 59 | index++; 60 | 61 | if(word.length() > 0){ 62 | // find word in clock string 63 | positionOfWord = clockStringGerman.indexOf(word, lastLetterClock); 64 | 65 | if(positionOfWord >= 0){ 66 | // word found on clock -> enable leds in targetgrid 67 | for(unsigned int i = 0; i < word.length(); i++){ 68 | int x = (positionOfWord + i)%WIDTH; 69 | int y = (positionOfWord + i)/WIDTH; 70 | ledmatrix.gridAddPixel(x, y, color); 71 | } 72 | // remember end of the word on clock 73 | lastLetterClock = positionOfWord + word.length(); 74 | } 75 | else{ 76 | // word is not possible to show on clock 77 | logger.logString("word is not possible to show on clock: " + String(word)); 78 | return -1; 79 | } 80 | }else{ 81 | // end - no more word in message 82 | break; 83 | } 84 | } 85 | // return success 86 | return 0; 87 | } 88 | 89 | /** 90 | * @brief Converts the given time as sentence (String) 91 | * 92 | * @param hours hours of the time value 93 | * @param minutes minutes of the time value 94 | * @return String time as sentence 95 | */ 96 | String timeToString(uint8_t hours,uint8_t minutes){ 97 | 98 | //ES IST 99 | String message = "ES IST "; 100 | 101 | 102 | //show minutes 103 | if(minutes >= 5 && minutes < 10) 104 | { 105 | message += "FUNF NACH "; 106 | } 107 | else if(minutes >= 10 && minutes < 15) 108 | { 109 | message += "ZEHN NACH "; 110 | } 111 | else if(minutes >= 15 && minutes < 20) 112 | { 113 | message += "VIERTEL NACH "; 114 | } 115 | else if(minutes >= 20 && minutes < 25) 116 | { 117 | message += "ZEHN VOR HALB "; 118 | } 119 | else if(minutes >= 25 && minutes < 30) 120 | { 121 | message += "FUNF VOR HALB "; 122 | } 123 | else if(minutes >= 30 && minutes < 35) 124 | { 125 | message += "HALB "; 126 | } 127 | else if(minutes >= 35 && minutes < 40) 128 | { 129 | message += "FUNF NACH HALB "; 130 | } 131 | else if(minutes >= 40 && minutes < 45) 132 | { 133 | message += "ZEHN NACH HALB "; 134 | } 135 | else if(minutes >= 45 && minutes < 50) 136 | { 137 | message += "VIERTEL VOR "; 138 | } 139 | else if(minutes >= 50 && minutes < 55) 140 | { 141 | message += "ZEHN VOR "; 142 | } 143 | else if(minutes >= 55 && minutes < 60) 144 | { 145 | message += "FUNF VOR "; 146 | } 147 | 148 | //convert hours to 12h format 149 | if(hours >= 12) 150 | { 151 | hours -= 12; 152 | } 153 | if(minutes >= 20) 154 | { 155 | hours++; 156 | } 157 | if(hours == 12) 158 | { 159 | hours = 0; 160 | } 161 | 162 | // show hours 163 | switch(hours) 164 | { 165 | case 0: 166 | message += "ZWOLF "; 167 | break; 168 | case 1: 169 | message += "EIN"; 170 | //EIN(S) 171 | if(minutes > 4){ 172 | message += "S"; 173 | } 174 | message += " "; 175 | break; 176 | case 2: 177 | message += "ZWEI "; 178 | break; 179 | case 3: 180 | message += "DREI "; 181 | break; 182 | case 4: 183 | message += "VIER "; 184 | break; 185 | case 5: 186 | message += "FUNF "; 187 | break; 188 | case 6: 189 | message += "SECHS "; 190 | break; 191 | case 7: 192 | message += "SIEBEN "; 193 | break; 194 | case 8: 195 | message += "ACHT "; 196 | break; 197 | case 9: 198 | message += "NEUN "; 199 | break; 200 | case 10: 201 | message += "ZEHN "; 202 | break; 203 | case 11: 204 | message += "ELF "; 205 | break; 206 | } 207 | if(minutes < 5) 208 | { 209 | message += "UHR "; 210 | } 211 | 212 | logger.logString("time as String: " + String(message)); 213 | 214 | return message; 215 | } 216 | 217 | -------------------------------------------------------------------------------- /wordclockfunctions.ino_english: -------------------------------------------------------------------------------- 1 | 2 | const String clockStringEnglish = "ITPISKTENNPQUARTERHALFTWENTYUFIVEMINUTESNATOPASTMEAONEFTWONTHREELRFOUREAWFIVEOSIXZUSEVENEIGHTELEVENUNINETWELVETENAWOCLOCK"; 3 | 4 | /** 5 | * @brief control the four minute indicator LEDs 6 | * 7 | * @param minutes minutes to be displayed [0 ... 59] 8 | * @param color 24bit color value 9 | */ 10 | void drawMinuteIndicator(uint8_t minutes, uint32_t color) { 11 | //separate LEDs for minutes in an additional row 12 | { 13 | switch (minutes % 5) { 14 | case 0: 15 | break; 16 | 17 | case 1: 18 | ledmatrix.setMinIndicator(0b1000, color); 19 | break; 20 | 21 | case 2: 22 | ledmatrix.setMinIndicator(0b1100, color); 23 | break; 24 | 25 | case 3: 26 | ledmatrix.setMinIndicator(0b1110, color); 27 | break; 28 | 29 | case 4: 30 | ledmatrix.setMinIndicator(0b1111, color); 31 | break; 32 | } 33 | } 34 | } 35 | 36 | /** 37 | * @brief Draw the given sentence to the word clock 38 | * 39 | * @param message sentence to be displayed 40 | * @param color 24bit color value 41 | * @return int: 0 if successful, -1 if sentence not possible to display 42 | */ 43 | int showStringOnClock(String message, uint32_t color) { 44 | String word = ""; 45 | int lastLetterClock = 0; 46 | int positionOfWord = 0; 47 | int index = 0; 48 | 49 | // add space on the end of message for splitting 50 | message = message + " "; 51 | 52 | // empty the targetgrid 53 | ledmatrix.gridFlush(); 54 | 55 | while (true) { 56 | // extract next word from message 57 | word = split(message, ' ', index); 58 | index++; 59 | 60 | if (word.length() > 0) { 61 | // find word in clock string 62 | positionOfWord = clockStringEnglish.indexOf(word, lastLetterClock); 63 | 64 | if (positionOfWord >= 0) { 65 | // word found on clock -> enable leds in targetgrid 66 | for (unsigned int i = 0; i < word.length(); i++) { 67 | int x = (positionOfWord + i) % WIDTH; 68 | int y = (positionOfWord + i) / WIDTH; 69 | ledmatrix.gridAddPixel(x, y, color); 70 | } 71 | // remember end of the word on clock 72 | lastLetterClock = positionOfWord + word.length(); 73 | } else { 74 | // word is not possible to show on clock 75 | logger.logString("word is not possible to show on clock: " + String(word)); 76 | return -1; 77 | } 78 | } else { 79 | // end - no more word in message 80 | break; 81 | } 82 | } 83 | // return success 84 | return 0; 85 | } 86 | 87 | /** 88 | * @brief Converts the given time as sentence (String) 89 | * 90 | * @param hours hours of the time value 91 | * @param minutes minutes of the time value 92 | * @return String time as sentence 93 | */ 94 | String timeToString(uint8_t hours, uint8_t minutes) { 95 | 96 | //IT IS 97 | String message = "IT IS "; 98 | 99 | 100 | //show minutes 101 | if (minutes >= 5 && minutes < 10) { 102 | message += "FIVE MINUTES "; 103 | } else if (minutes >= 10 && minutes < 15) { 104 | message += "TEN MINUTES "; 105 | } else if (minutes >= 15 && minutes < 20) { 106 | message += "QUARTER "; 107 | } else if (minutes >= 20 && minutes < 25) { 108 | message += "TWENTY MINUTES "; 109 | } else if (minutes >= 25 && minutes < 30) { 110 | message += "TWENTY FIVE MINUTES "; 111 | } else if (minutes >= 30 && minutes < 35) { 112 | message += "HALF "; 113 | } else if (minutes >= 35 && minutes < 40) { 114 | message += "TWENTY FIVE MINUTES "; 115 | } else if (minutes >= 40 && minutes < 45) { 116 | message += "TWENTY MINUTES "; 117 | } else if (minutes >= 45 && minutes < 50) { 118 | message += "QUARTER "; 119 | } else if (minutes >= 50 && minutes < 55) { 120 | message += "TEN MINUTES "; 121 | } else if (minutes >= 55 && minutes < 60) { 122 | message += "FIVE MINUTES "; 123 | } 124 | 125 | // Convert hours to 12h format and adjust for "TO" phrases 126 | if (hours >= 12) { 127 | hours -= 12; 128 | } 129 | 130 | // Increment hour for "TO" phrases (minutes 35 or more) 131 | if (minutes >= 35) { 132 | hours = (hours + 1) % 12; 133 | message += "TO "; 134 | } else if (minutes >= 5) { 135 | message += "PAST "; 136 | } 137 | 138 | // Handle edge case for 0 hour (12 AM/PM) 139 | if (hours == 0) { 140 | hours = 12; 141 | } 142 | 143 | // show hours 144 | switch (hours) { 145 | case 1: 146 | message += "ONE "; 147 | break; 148 | case 2: 149 | message += "TWO "; 150 | break; 151 | case 3: 152 | message += "THREE "; 153 | break; 154 | case 4: 155 | message += "FOUR "; 156 | break; 157 | case 5: 158 | message += "FIVE "; 159 | break; 160 | case 6: 161 | message += "SIX "; 162 | break; 163 | case 7: 164 | message += "SEVEN "; 165 | break; 166 | case 8: 167 | message += "EIGHT "; 168 | break; 169 | case 9: 170 | message += "NINE "; 171 | break; 172 | case 10: 173 | message += "TEN "; 174 | break; 175 | case 11: 176 | message += "ELEVEN "; 177 | break; 178 | case 12: 179 | message += "TWELVE "; 180 | break; 181 | } 182 | 183 | if (minutes < 5) { 184 | message += "OCLOCK "; 185 | } 186 | 187 | logger.logString("time as String: " + String(message)); 188 | 189 | return message; 190 | } 191 | -------------------------------------------------------------------------------- /wordclockfunctions.ino_french: -------------------------------------------------------------------------------- 1 | 2 | const String clockStringFrench = "ILOESTZRTUEDEUXSTTROISQUATRETNEUFUNESEPTHUITSIXDIXKONZECINQDHEURESMIDIRMINUITMOINSAECINQETRQUARTDIXVINGT-CINQEETKDEMIEILI"; 3 | 4 | /** 5 | * @brief control the four minute indicator LEDs 6 | * 7 | * @param minutes minutes to be displayed [0 ... 59] 8 | * @param color 24bit color value 9 | */ 10 | void drawMinuteIndicator(uint8_t minutes, uint32_t color){ 11 | //separate LEDs for minutes in an additional row 12 | { 13 | switch (minutes%5) 14 | { 15 | case 0: 16 | break; 17 | 18 | case 1: 19 | ledmatrix.setMinIndicator(0b1000, color); 20 | break; 21 | 22 | case 2: 23 | ledmatrix.setMinIndicator(0b1100, color); 24 | break; 25 | 26 | case 3: 27 | ledmatrix.setMinIndicator(0b1110, color); 28 | break; 29 | 30 | case 4: 31 | ledmatrix.setMinIndicator(0b1111, color); 32 | break; 33 | } 34 | } 35 | } 36 | 37 | /** 38 | * @brief Draw the given sentence to the word clock 39 | * 40 | * @param message sentence to be displayed 41 | * @param color 24bit color value 42 | * @return int: 0 if successful, -1 if sentence not possible to display 43 | */ 44 | int showStringOnClock(String message, uint32_t color){ 45 | String word = ""; 46 | int lastLetterClock = 0; 47 | int positionOfWord = 0; 48 | int index = 0; 49 | 50 | // add space on the end of message for splitting 51 | message = message + " "; 52 | 53 | // empty the targetgrid 54 | ledmatrix.gridFlush(); 55 | 56 | while(true){ 57 | // extract next word from message 58 | word = split(message, ' ', index); 59 | index++; 60 | 61 | if(word.length() > 0){ 62 | // find word in clock string 63 | positionOfWord = clockStringFrench.indexOf(word, lastLetterClock); 64 | 65 | if(positionOfWord >= 0){ 66 | // word found on clock -> enable leds in targetgrid 67 | for(unsigned int i = 0; i < word.length(); i++){ 68 | int x = (positionOfWord + i)%WIDTH; 69 | int y = (positionOfWord + i)/WIDTH; 70 | ledmatrix.gridAddPixel(x, y, color); 71 | } 72 | // remember end of the word on clock 73 | lastLetterClock = positionOfWord + word.length(); 74 | } 75 | else{ 76 | // word is not possible to show on clock 77 | logger.logString("word is not possible to show on clock: " + String(word)); 78 | return -1; 79 | } 80 | }else{ 81 | // end - no more word in message 82 | break; 83 | } 84 | } 85 | // return success 86 | return 0; 87 | } 88 | 89 | /** 90 | * @brief convert the given number to french 91 | * 92 | * @param number number to be converted 93 | * @return String number as french 94 | */ 95 | String numberToFrench(uint8_t number) { 96 | switch (number) { 97 | case 1: return "UNE"; 98 | case 2: return "DEUX"; 99 | case 3: return "TROIS"; 100 | case 4: return "QUATRE"; 101 | case 5: return "CINQ"; 102 | case 6: return "SIX"; 103 | case 7: return "SEPT"; 104 | case 8: return "HUIT"; 105 | case 9: return "NEUF"; 106 | case 10: return "DIX"; 107 | case 11: return "ONZE"; 108 | case 12: return "DOUZE"; 109 | default: return ""; 110 | } 111 | } 112 | 113 | /** 114 | * @brief Converts the given time as sentence (String) 115 | * 116 | * @param hours hours of the time value 117 | * @param minutes minutes of the time value 118 | * @return String time as sentence 119 | */ 120 | String timeToString(uint8_t hours, uint8_t minutes) { 121 | // Rounding the minutes to the next 5-minute cycle 122 | minutes = minutes / 5 * 5; 123 | 124 | String message = "IL EST "; 125 | 126 | if(minutes >= 35) 127 | { 128 | hours++; 129 | } 130 | 131 | if ((hours == 0 && minutes <= 30) || (hours == 24 && minutes >= 35)) { 132 | message += "MINUIT"; 133 | } else if ((hours == 12 && minutes <= 30) || (hours == 12 && minutes >=35)) { 134 | message += "MIDI"; 135 | } else { 136 | uint8_t hours12h = hours; 137 | if (hours12h > 12) { 138 | hours12h -= 12; 139 | } 140 | message += numberToFrench(hours12h) + " HEURE" + (hours12h > 1 ? "S" : ""); 141 | } 142 | 143 | // Format minutes 144 | if (minutes == 0) { 145 | // No minutes, only full hours 146 | return message; 147 | } else if (minutes == 5) { 148 | message += " CINQ"; 149 | } else if (minutes == 10) { 150 | message += " DIX"; 151 | } else if (minutes == 15) { 152 | message += " ET QUART"; 153 | } else if (minutes == 20) { 154 | message += " VINGT"; 155 | } else if (minutes == 25) { 156 | message += " VINGT-CINQ"; 157 | } else if (minutes == 30) { 158 | message += " ET DEMI" + String((hours == 0 || hours == 12 || hours == 13) ? "" : "E"); 159 | } else if (minutes == 35) { 160 | message += " MOINS VINGT-CINQ"; 161 | } else if (minutes == 40) { 162 | message += " MOINS VINGT"; 163 | } else if (minutes == 45) { 164 | message += " MOINS QUART"; 165 | } else if (minutes == 50) { 166 | message += " MOINS DIX"; 167 | } else if (minutes == 55) { 168 | message += " MOINS CINQ"; 169 | } 170 | 171 | return message; 172 | } 173 | 174 | -------------------------------------------------------------------------------- /wordclockfunctions.ino_italian: -------------------------------------------------------------------------------- 1 | // HINT: I replaced the special Italian letters (E' and L') in the following strings with = and # 2 | const String clockStringItalian = "SONORLEBORE=R#UNASDUEZTREOTTONOVEDIECIUNDICIDODICISETTEQUATTROCSEICINQUEAMENOECUNOQUARTOVENTICINQUELVETENAWOCLDIECIPMEZZA"; 3 | 4 | /** 5 | * @brief control the four minute indicator LEDs 6 | * 7 | * @param minutes minutes to be displayed [0 ... 59] 8 | * @param color 24bit color value 9 | */ 10 | void drawMinuteIndicator(uint8_t minutes, uint32_t color){ 11 | //separate LEDs for minutes in an additional row 12 | { 13 | switch (minutes%5) 14 | { 15 | case 0: 16 | break; 17 | 18 | case 1: 19 | ledmatrix.setMinIndicator(0b1000, color); 20 | break; 21 | 22 | case 2: 23 | ledmatrix.setMinIndicator(0b1100, color); 24 | break; 25 | 26 | case 3: 27 | ledmatrix.setMinIndicator(0b1110, color); 28 | break; 29 | 30 | case 4: 31 | ledmatrix.setMinIndicator(0b1111, color); 32 | break; 33 | } 34 | } 35 | } 36 | 37 | /** 38 | * @brief Draw the given sentence to the word clock 39 | * 40 | * @param message sentence to be displayed 41 | * @param color 24bit color value 42 | * @return int: 0 if successful, -1 if sentence not possible to display 43 | */ 44 | int showStringOnClock(String message, uint32_t color){ 45 | String word = ""; 46 | int lastLetterClock = 0; 47 | int positionOfWord = 0; 48 | int index = 0; 49 | 50 | // add space on the end of message for splitting 51 | message = message + " "; 52 | 53 | // empty the targetgrid 54 | ledmatrix.gridFlush(); 55 | 56 | while(true){ 57 | // extract next word from message 58 | word = split(message, ' ', index); 59 | index++; 60 | 61 | if(word.length() > 0){ 62 | // find word in clock string 63 | positionOfWord = clockStringItalian.indexOf(word, lastLetterClock); 64 | 65 | if(positionOfWord >= 0){ 66 | // word found on clock -> enable leds in targetgrid 67 | for(unsigned int i = 0; i < word.length(); i++){ 68 | int x = (positionOfWord + i)%WIDTH; 69 | int y = (positionOfWord + i)/WIDTH; 70 | ledmatrix.gridAddPixel(x, y, color); 71 | } 72 | // remember end of the word on clock 73 | lastLetterClock = positionOfWord + word.length(); 74 | } 75 | else{ 76 | // word is not possible to show on clock 77 | logger.logString("word is not possible to show on clock: " + String(word)); 78 | return -1; 79 | } 80 | }else{ 81 | // end - no more word in message 82 | break; 83 | } 84 | } 85 | // return success 86 | return 0; 87 | } 88 | 89 | /** 90 | * @brief Converts the given time as sentence (String) 91 | * 92 | * @param hours hours of the time value 93 | * @param minutes minutes of the time value 94 | * @return String time as sentence 95 | */ 96 | String timeToString(uint8_t hours,uint8_t minutes){ 97 | 98 | //IT IS 99 | String message = ""; 100 | 101 | 102 | //convert hours to 12h format 103 | if(hours >= 12) 104 | { 105 | hours -= 12; 106 | } 107 | if(minutes >= 35) 108 | { 109 | hours++; 110 | } 111 | if(hours == 12) 112 | { 113 | hours = 0; 114 | } 115 | 116 | //SONO LE 117 | if(hours == 1) 118 | { 119 | message += "= # "; //E' L' -> = is for E' and # is for L' 120 | } 121 | else 122 | { 123 | message += "SONO LE "; 124 | } 125 | 126 | // show hours 127 | switch(hours) 128 | { 129 | case 0: 130 | message += "DODICI "; 131 | break; 132 | case 1: 133 | message += "UNA "; 134 | break; 135 | case 2: 136 | message += "DUE "; 137 | break; 138 | case 3: 139 | message += "TRE "; 140 | break; 141 | case 4: 142 | message += "QUATTRO "; 143 | break; 144 | case 5: 145 | message += "CINQUE "; 146 | break; 147 | case 6: 148 | message += "SEI "; 149 | break; 150 | case 7: 151 | message += "SETTE "; 152 | break; 153 | case 8: 154 | message += "OTTO "; 155 | break; 156 | case 9: 157 | message += "NOVE "; 158 | break; 159 | case 10: 160 | message += "DIECI "; 161 | break; 162 | case 11: 163 | message += "UNDICI "; 164 | break; 165 | } 166 | 167 | 168 | 169 | //show minutes 170 | if(minutes >= 5 && minutes < 10) 171 | { 172 | message += "E CINQUE "; 173 | } 174 | else if(minutes >= 10 && minutes < 15) 175 | { 176 | message += "E DIECI "; 177 | } 178 | else if(minutes >= 15 && minutes < 20) 179 | { 180 | message += "E UN QUARTO "; 181 | } 182 | else if(minutes >= 20 && minutes < 25) 183 | { 184 | message += "E VENTI "; 185 | } 186 | else if(minutes >= 25 && minutes < 30) 187 | { 188 | message += "E VENTICINQUE "; 189 | } 190 | else if(minutes >= 30 && minutes < 35) 191 | { 192 | message += "E MEZZA "; 193 | } 194 | else if(minutes >= 35 && minutes < 40) 195 | { 196 | message += "MENO VENTICINQUE "; 197 | } 198 | else if(minutes >= 40 && minutes < 45) 199 | { 200 | message += "MENO VENTI "; 201 | } 202 | else if(minutes >= 45 && minutes < 50) 203 | { 204 | message += "MENO UN QUARTO "; 205 | } 206 | else if(minutes >= 50 && minutes < 55) 207 | { 208 | message += "MENO DIECI "; 209 | } 210 | else if(minutes >= 55 && minutes < 60) 211 | { 212 | message += "MENO CINQUE "; 213 | } 214 | 215 | logger.logString("time as String: " + String(message)); 216 | 217 | return message; 218 | } 219 | 220 | -------------------------------------------------------------------------------- /wordclockfunctions.ino_javanese: -------------------------------------------------------------------------------- 1 | 2 | const String clockStringJava = "JAMASETENGAHTELOROLASIJIPAPATELUENEMPITUWOLULIMOSEPULOHSONGOOSEWELASPASIKURANGPUNJULLIMOSEPRAPATKATESEPULOHIJURONGPULOHATSELAWEMENIT"; 3 | 4 | /** 5 | * @brief control the four minute indicator LEDs 6 | * 7 | * @param minutes minutes to be displayed [0 ... 59] 8 | * @param color 24bit color value 9 | */ 10 | void drawMinuteIndicator(uint8_t minutes, uint32_t color) { 11 | //separate LEDs for minutes in an additional row 12 | { 13 | switch (minutes % 5) { 14 | case 0: 15 | break; 16 | 17 | case 1: 18 | ledmatrix.setMinIndicator(0b1000, color); 19 | break; 20 | 21 | case 2: 22 | ledmatrix.setMinIndicator(0b1100, color); 23 | break; 24 | 25 | case 3: 26 | ledmatrix.setMinIndicator(0b1110, color); 27 | break; 28 | 29 | case 4: 30 | ledmatrix.setMinIndicator(0b1111, color); 31 | break; 32 | } 33 | } 34 | } 35 | 36 | /** 37 | * @brief Draw the given sentence to the word clock 38 | * 39 | * @param message sentence to be displayed 40 | * @param color 24bit color value 41 | * @return int: 0 if successful, -1 if sentence not possible to display 42 | */ 43 | int showStringOnClock(String message, uint32_t color) { 44 | String word = ""; 45 | int lastLetterClock = 0; 46 | int positionOfWord = 0; 47 | int index = 0; 48 | 49 | // add space on the end of message for splitting 50 | message = message + " "; 51 | 52 | // empty the targetgrid 53 | ledmatrix.gridFlush(); 54 | 55 | while (true) { 56 | // extract next word from message 57 | word = split(message, ' ', index); 58 | index++; 59 | 60 | if (word.length() > 0) { 61 | // find word in clock string 62 | positionOfWord = clockStringJava.indexOf(word, lastLetterClock); 63 | 64 | if (positionOfWord >= 0) { 65 | // word found on clock -> enable leds in targetgrid 66 | for (unsigned int i = 0; i < word.length(); i++) { 67 | int x = (positionOfWord + i) % WIDTH; 68 | int y = (positionOfWord + i) / WIDTH; 69 | ledmatrix.gridAddPixel(x, y, color); 70 | } 71 | // remember end of the word on clock 72 | lastLetterClock = positionOfWord + word.length(); 73 | } else { 74 | // word is not possible to show on clock 75 | logger.logString("word is not possible to show on clock: " + String(word)); 76 | return -1; 77 | } 78 | } else { 79 | // end - no more word in message 80 | break; 81 | } 82 | } 83 | // return success 84 | return 0; 85 | } 86 | 87 | /** 88 | * @brief Converts the given time as sentence (String) 89 | * 90 | * @param hours hours of the time value 91 | * @param minutes minutes of the time value 92 | * @return String time as sentence 93 | */ 94 | String timeToString(uint8_t hours, uint8_t minutes) { 95 | 96 | String message = "JAM "; 97 | 98 | hours = hours % 12; 99 | 100 | if (minutes >= 30) 101 | hours = (hours + 1) % 12; 102 | 103 | if (hours == 0) 104 | hours = 12; 105 | 106 | if (minutes >= 30 && minutes < 35) 107 | message += "SETENGAH "; 108 | 109 | // show hours 110 | switch (hours) { 111 | case 1: 112 | message += "SIJI "; 113 | break; 114 | case 2: 115 | message += "LORO "; 116 | break; 117 | case 3: 118 | message += "TELU "; 119 | break; 120 | case 4: 121 | message += "PAPAT "; 122 | break; 123 | case 5: 124 | message += "LIMO "; 125 | break; 126 | case 6: 127 | message += "ENEM "; 128 | break; 129 | case 7: 130 | message += "PITU "; 131 | break; 132 | case 8: 133 | message += "WOLU "; 134 | break; 135 | case 9: 136 | message += "SONGO "; 137 | break; 138 | case 10: 139 | message += "SEPULOH "; 140 | break; 141 | case 11: 142 | message += "SEWELAS "; 143 | break; 144 | case 12: 145 | message += "ROLAS "; 146 | break; 147 | } 148 | 149 | if (minutes >= 35) { 150 | message += "KURANG "; 151 | } else if (minutes >= 5 && minutes < 30) { 152 | message += "PUNJUL "; 153 | } 154 | 155 | //show minutes 156 | if (minutes >= 5 && minutes < 10) { 157 | message += "LIMO MENIT "; 158 | } else if (minutes >= 10 && minutes < 15) { 159 | message += "SEPULOH MENIT "; 160 | } else if (minutes >= 15 && minutes < 20) { 161 | message += "SEPRAPAT MENIT"; 162 | } else if (minutes >= 20 && minutes < 25) { 163 | message += "RONGPULOH MENIT "; 164 | } else if (minutes >= 25 && minutes < 30) { 165 | message += "SELAWE MENIT "; 166 | } else if (minutes >= 30 && minutes < 35) { 167 | // message += "SETENGAH "; 168 | } else if (minutes >= 35 && minutes < 40) { 169 | message += "SELAWE MENIT "; 170 | } else if (minutes >= 40 && minutes < 45) { 171 | message += "RONGPULOH MENIT "; 172 | } else if (minutes >= 45 && minutes < 50) { 173 | message += "SEPRAPAT MENIT "; 174 | } else if (minutes >= 50 && minutes < 55) { 175 | message += "SEPULOH MENIT "; 176 | } else if (minutes >= 55 && minutes < 60) { 177 | message += "LIMO MENIT "; 178 | } 179 | 180 | 181 | if (minutes < 5 || (minutes >= 30 && minutes < 35)) { 182 | message += "PAS "; 183 | } 184 | 185 | logger.logString("time as String: " + String(message)); 186 | 187 | return message; 188 | } 189 | -------------------------------------------------------------------------------- /wordclockfunctions.ino_swiss: -------------------------------------------------------------------------------- 1 | // Thanks to Sandro for providing this swiss german version 2 | const String clockStringSwiss = "ESPESCHAFUFVIERTUBFZAAZWANZGSIVORABOHWORTUHRHAUBIANESSIEISZWOISDRUVIERIYFUFIOSACHSISEBNIACHTINUNIELZANIERBEUFIZWOUFINAGSI"; 3 | 4 | /** 5 | * @brief control the four minute indicator LEDs 6 | * 7 | * @param minutes minutes to be displayed [0 ... 59] 8 | * @param color 24bit color value 9 | */ 10 | void drawMinuteIndicator(uint8_t minutes, uint32_t color){ 11 | //separate LEDs for minutes in an additional row 12 | { 13 | switch (minutes%5) 14 | { 15 | case 0: 16 | break; 17 | 18 | case 1: 19 | ledmatrix.setMinIndicator(0b1000, color); 20 | break; 21 | 22 | case 2: 23 | ledmatrix.setMinIndicator(0b1100, color); 24 | break; 25 | 26 | case 3: 27 | ledmatrix.setMinIndicator(0b1110, color); 28 | break; 29 | 30 | case 4: 31 | ledmatrix.setMinIndicator(0b1111, color); 32 | break; 33 | } 34 | } 35 | } 36 | 37 | /** 38 | * @brief Draw the given sentence to the word clock 39 | * 40 | * @param message sentence to be displayed 41 | * @param color 24bit color value 42 | * @return int: 0 if successful, -1 if sentence not possible to display 43 | */ 44 | int showStringOnClock(String message, uint32_t color){ 45 | String word = ""; 46 | int lastLetterClock = 0; 47 | int positionOfWord = 0; 48 | int index = 0; 49 | 50 | // add space on the end of message for splitting 51 | message = message + " "; 52 | 53 | // empty the targetgrid 54 | ledmatrix.gridFlush(); 55 | 56 | while(true){ 57 | // extract next word from message 58 | word = split(message, ' ', index); 59 | index++; 60 | 61 | if(word.length() > 0){ 62 | // find word in clock string 63 | positionOfWord = clockStringSwiss.indexOf(word, lastLetterClock); 64 | 65 | if(positionOfWord >= 0){ 66 | // word found on clock -> enable leds in targetgrid 67 | for(unsigned int i = 0; i < word.length(); i++){ 68 | int x = (positionOfWord + i)%WIDTH; 69 | int y = (positionOfWord + i)/WIDTH; 70 | ledmatrix.gridAddPixel(x, y, color); 71 | } 72 | // remember end of the word on clock 73 | lastLetterClock = positionOfWord + word.length(); 74 | } 75 | else{ 76 | // word is not possible to show on clock 77 | logger.logString("word is not possible to show on clock: " + String(word)); 78 | return -1; 79 | } 80 | }else{ 81 | // end - no more word in message 82 | break; 83 | } 84 | } 85 | // return success 86 | return 0; 87 | } 88 | 89 | /** 90 | * @brief Converts the given time as sentence (String) 91 | * 92 | * @param hours hours of the time value 93 | * @param minutes minutes of the time value 94 | * @return String time as sentence 95 | */ 96 | String timeToString(uint8_t hours, uint8_t minutes, bool puristModeActive){ 97 | 98 | //ES IST 99 | String message = ""; 100 | 101 | if(puristModeActive){ 102 | message = ""; 103 | if(minutes < 5 || (minutes >=30 && minutes < 35)){ 104 | message = "ES ESCH "; 105 | } 106 | } 107 | else{ 108 | message = "ES ESCH "; 109 | } 110 | 111 | //show minutes 112 | if(minutes >= 5 && minutes < 10) 113 | { 114 | message += "FUF AB "; 115 | } 116 | else if(minutes >= 10 && minutes < 15) 117 | { 118 | message += "ZAA AB "; 119 | } 120 | else if(minutes >= 15 && minutes < 20) 121 | { 122 | message += "VIERTU AB "; 123 | } 124 | else if(minutes >= 20 && minutes < 25) 125 | { 126 | message += "ZWANZG AB "; //Sandro 127 | } 128 | else if(minutes >= 25 && minutes < 30) 129 | { 130 | message += "FUF VOR HAUBI "; 131 | } 132 | else if(minutes >= 30 && minutes < 35) 133 | { 134 | message += "HAUBI "; 135 | } 136 | else if(minutes >= 35 && minutes < 40) 137 | { 138 | message += "FUF AB HAUBI "; 139 | } 140 | else if(minutes >= 40 && minutes < 45) 141 | { 142 | message += "ZWANZG VOR "; //Sandro 143 | } 144 | else if(minutes >= 45 && minutes < 50) 145 | { 146 | message += "VIERTU VOR "; 147 | } 148 | else if(minutes >= 50 && minutes < 55) 149 | { 150 | message += "ZAA VOR "; 151 | } 152 | else if(minutes >= 55 && minutes < 60) 153 | { 154 | message += "FUF VOR "; 155 | } 156 | 157 | //convert hours to 12h format 158 | if(hours >= 12) 159 | { 160 | hours -= 12; 161 | } 162 | if(minutes >= 25) //Sandro 20 163 | { 164 | hours++; 165 | } 166 | if(hours == 12) 167 | { 168 | hours = 0; 169 | } 170 | 171 | // show hours 172 | switch(hours) 173 | { 174 | case 0: 175 | message += "ZWOUFI "; 176 | break; 177 | case 1: 178 | message += "EIS "; 179 | // //EIN(S) 180 | // if(minutes > 4){ // Sandro 181 | // message += "S"; 182 | // } 183 | // message += " "; 184 | break; 185 | case 2: 186 | message += "ZWOI "; 187 | break; 188 | case 3: 189 | message += "DRU "; 190 | break; 191 | case 4: 192 | message += "VIERI "; 193 | break; 194 | case 5: 195 | message += "FUFI "; 196 | break; 197 | case 6: 198 | message += "SACHSI "; 199 | break; 200 | case 7: 201 | message += "SEBNI "; 202 | break; 203 | case 8: 204 | message += "ACHTI "; 205 | break; 206 | case 9: 207 | message += "NUNI "; 208 | break; 209 | case 10: 210 | message += "ZANI "; 211 | break; 212 | case 11: 213 | message += "EUFI "; 214 | break; 215 | } 216 | if(minutes < 5) 217 | { 218 | message += "GSI "; 219 | } 220 | 221 | logger.logString("time as String: " + String(message)); 222 | 223 | return message; 224 | } 225 | 226 | --------------------------------------------------------------------------------