commit 9a4a8c2a7d6b4a111c1dbbc26b8070532a23e5bd Author: Markus Ransberger Date: Mon Aug 21 20:26:38 2023 +0200 Initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dccc9c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,65 @@ +# PlatformIO +.pio + +# VS Code +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch + +# Own folders +_unused/ +res/ + +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..be081b2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Techniccontroller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f47ca57 --- /dev/null +++ b/README.md @@ -0,0 +1,144 @@ +# Wordclock 2.0 + +Wordclock 2.0 with ESP8266 and NTP time + +More details on my website: https://techniccontroller.com/word-clock-with-wifi-and-neopixel/ + + +**Languages** + +The Wordclock is available in **German**, **English** and **Italian** language. By default the language is German. +To use the English or Italian language please replace the file *wordclockfunctions.ino* with *wordclockfunctions.ino_english* or *wordclockfunctions.ino_italian*. +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. + + +## Features +- 6 modes (Clock, Digital Clock, SPIRAL animation, TETRIS, SNAKE, PONG) +- time update via NTP server +- automatic summer/wintertime change +- easy WIFI setup with WifiManager +- configurable color +- configurable night mode (start and end time) +- configurable brightness +- automatic mode change +- webserver interface for configuration and control +- physical button to change mode or enable night mode without webserver +- automatic current limiting of LEDs + +## Pictures of clock +![modes_images2](https://user-images.githubusercontent.com/36072504/156947689-dd90874d-a887-4254-bede-4947152d85c1.png) + +## Screenshots of webserver UI +![screenshots_UI](https://user-images.githubusercontent.com/36072504/158478447-d828e460-d4eb-489e-981e-216e08d4b129.png) + +## Quickstart + +1. Clone the project into the sketch folder of the Arduino IDE, +2. Rename the file "example_secrets.h" to "secrets.h". You don't need to change anything in the file if you want uses the normal WiFi setup with WiFiManager (see section "Remark about the WiFi setup"). +3. Install the additional libraries and flash it 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). +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. +5. After a successful WiFi setup, open the browser and enter the IP address of your ESP8266 to access the interface of the webserver. +6. Here you can then upload all files located in the folder "data". Please make sure all icons stay in the folder "icons" also on the webserver. + + + + +## Install needed Libraries + +Please download all these libraries as ZIP from GitHub, and extract them in the *libraries* folder of your Sketchbook location (see **File -> Preferences**): + +- https://github.com/adafruit/Adafruit-GFX-Library +- https://github.com/adafruit/Adafruit_NeoMatrix +- https://github.com/adafruit/Adafruit_NeoPixel +- https://github.com/tzapu/WiFiManager +- https://github.com/adafruit/Adafruit_BusIO + +folder structure should look like this: + +``` +MySketchbookLocation +│ +└───libraries +│ └───Adafruit-GFX-Library +│ └───Adafruit_NeoMatrix +│ └───Adafruit_NeoPixel +│ └───WiFiManager +│ └───Adafruit_BusIO +│ +└───wordclock_esp8266 + │ wordclock_esp8266.ino + │ (...) + | + └───data + │ index.html + | (...) + | + └───icons +``` + + +## Upload program to ESP8266 with Arduino IDE + +#### STEP1: Installation of Arduino IDE +First, the latest version of the Arduino IDE needs to be downloaded and installed from here. + +#### STEP2: Installation of ESP8266 Arduino Core +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: + +- Start Arduino and open the File -> Preferences window. + +- 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. +![image](https://user-images.githubusercontent.com/36072504/169649790-1b85660e-8c7d-4dfe-a63a-5dfd9862a5de.png) + +- Open Boards Manager from Tools > Board menu and search for "esp8266". + +- Click the install button. + +- Don’t forget to select your ESP8266 board from Tools > Board menu after installation (e.g NodeMCU 1.0) +![image](https://user-images.githubusercontent.com/36072504/169649801-898c4819-9145-45c5-b65b-52f2689ab646.png) + +#### STEP3: Upload a program to ESP8266 + +- Open wordclock_esp8266.ino in Arduino IDE +- Connect ESP8266 board with Computer +- Select right serial Port in Tools -> Port +- Click on the upload button in the Arduino IDE to upload the program to the ESP8266 Module. +![image](https://user-images.githubusercontent.com/36072504/169649810-1fda75c2-5f4d-4d71-98fe-30985d82f7f5.png) + + +## Remark about the WiFi setup + +Regarding the Wifi setting, I have actually implemented two variants: +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 simply connect to the cell phone and you can perform configuration of the WiFi settings conveniently as with a SmartHome devices (Very elegant 😊) +2. Another (traditional) variant is to define the wifi credentials in the code (in secrets.h). + - For this you have to comment out lines 230 to 251 in the code of the file *wordclock_esp8266.ino* (/\* before and \*/ after) + - and comment out lines 257 to 305 (/\* and \*/ remove) + + +## Remark about Logging + +The wordclock send continuously log messages to the serial port and via multicast UDP. If you want to see these messages, you have to + +- open the serial monitor in the Arduino IDE (Tools -> Serial Monitor). The serial monitor must be set to 115200 baud. + +OR + +- run the following steps for the multicast UDP logging: + +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). +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!). +```python +# ip address of network interface +MCAST_IF_IP = '192.168.0.7' +``` +4. execute the script with following command: + +```bash +python multicastUDP_receiver_analyzer.py +``` + +5. now you should see the log messages of the word clock (every 5 seconds a heartbeat message and the currently displayed time). +If this is not the case, there could be a problem with the network settings of the computer, then recording is unfortunately not possible. + +6. If special events (failed NTP update, reboot) occur, a section of the log is saved in a file called *log.txt*. +In principle, the events are not critical and will occur from time to time, but should not be too frequent. diff --git a/include/animationfunctions.h b/include/animationfunctions.h new file mode 100644 index 0000000..52fc491 --- /dev/null +++ b/include/animationfunctions.h @@ -0,0 +1,20 @@ +#ifndef ANIMATIONFUNCTIONS_H +#define ANIMATIONFUNCTIONS_H + +#include + +enum Direction +{ + RIGHT, + LEFT, + UP, + DOWN +}; + +Direction next_direction(Direction dir, int d); +int random_snake(bool init, const uint8_t len, const uint32_t color, int numSteps); +int random_tetris(bool init); +int draw_spiral(bool init, bool empty, uint8_t size); +void show_digital_clock(uint8_t hours, uint8_t minutes, uint32_t color); + +#endif /* ANIMATIONFUNCTIONS_H */ diff --git a/include/base64_wrapper.h b/include/base64_wrapper.h new file mode 100644 index 0000000..801f731 --- /dev/null +++ b/include/base64_wrapper.h @@ -0,0 +1,26 @@ +/* +Copyright (C) 2016 Arturo Guadalupi. All right reserved. + +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. + +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. +*/ + +#ifndef BASE64_WRAPPER_H +#define BASE64_WRAPPER_H + +class Base64Class{ + public: + int encode(char *output, char *input, int inputLength); + int decode(char * output, char * input, int inputLength); + int encodedLength(int plainLength); + int decodedLength(char * input, int inputLength); + + private: + inline void fromA3ToA4(unsigned char * A4, unsigned char * A3); + inline void fromA4ToA3(unsigned char * A3, unsigned char * A4); + inline unsigned char lookupTable(char c); +}; +extern Base64Class Base64; + +#endif /* BASE64_WRAPPER_H */ diff --git a/include/ledmatrix.h b/include/ledmatrix.h new file mode 100644 index 0000000..43ab1b0 --- /dev/null +++ b/include/ledmatrix.h @@ -0,0 +1,56 @@ +#ifndef LEDMATRIX_H +#define LEDMATRIX_H + +#include +#include +#include +#include "wordclock_constants.h" +#include "udp_logger.h" + +#define DEFAULT_CURRENT_LIMIT 9999 + +extern const uint32_t colors_24bit[NUM_COLORS]; + +class LEDMatrix +{ +public: + LEDMatrix(Adafruit_NeoMatrix * matrix, uint8_t brightness, UDPLogger * logger); + static uint16_t color_24_to_16bit(uint32_t color24bit); + static uint32_t color_24bit(uint8_t r, uint8_t g, uint8_t b); + static uint32_t interpolate_color_24bit(uint32_t color1, uint32_t color2, float factor); + static uint32_t wheel(uint8_t WheelPos); + void draw_on_matrix_instant(); + void draw_on_matrix_smooth(float factor); + void flush(void); + void grid_add_pixel(uint8_t x, uint8_t y, uint32_t color); + void print_char(uint8_t xpos, uint8_t ypos, char character, uint32_t color); + void print_number(uint8_t xpos, uint8_t ypos, uint8_t number, uint32_t color); + void set_brightness(uint8_t mybrightness); + void set_current_limit(uint16_t new_current_limit); + void set_min_indicator(uint8_t pattern, uint32_t color); + void setup_matrix(); + +private: + Adafruit_NeoMatrix * _neomatrix; + UDPLogger * _logger; + + uint8_t _brightness; + uint16_t _current_limit; + + // target representation of matrix as 2D array + uint32_t _target_grid[MATRIX_HEIGHT][MATRIX_WIDTH] = {0}; + + // current representation of matrix as 2D array + uint32_t _current_grid[MATRIX_HEIGHT][MATRIX_WIDTH] = {0}; + + // target representation of minutes indicator leds + uint32_t _target_indicators[4] = {0, 0, 0, 0}; + + // current representation of minutes indicator leds + uint32_t _current_indicators[4] = {0, 0, 0, 0}; + + void _draw_on_matrix(float factor); + uint16_t _calc_estimated_led_current(uint32_t color); +}; + +#endif /* LEDMATRIX_H */ \ No newline at end of file diff --git a/include/littlefs_wrapper.h b/include/littlefs_wrapper.h new file mode 100644 index 0000000..7847445 --- /dev/null +++ b/include/littlefs_wrapper.h @@ -0,0 +1,15 @@ +#ifndef LITTLEFS_WRAPPER_H +#define LITTLEFS_WRAPPER_H + +#include + +bool handle_file(String &&path); +bool handle_list(); +const String format_bytes(size_t const &bytes); +void delete_recursive(const String &path); +void format_filesystem(); +void handle_upload(); +void send_response(); +void setup_filesystem(); + +#endif /* LITTLEFS_WRAPPER_H */ diff --git a/include/ntp_client_plus.h b/include/ntp_client_plus.h new file mode 100644 index 0000000..e5e81d7 --- /dev/null +++ b/include/ntp_client_plus.h @@ -0,0 +1,90 @@ +#ifndef NTPCLIENTPLUS_H +#define NTPCLIENTPLUS_H + +#include +#include + +#define UNIX_TIMESTAMP_1900 2208988800UL // careful: positive value +#define NTP_PACKET_SIZE 48 +#define NTP_DEFAULT_LOCAL_PORT 1337 +#define MAX_NTP_CONN_TRIES 50 // 50 * NTP_RECEIVE_WAIT_TIME_MS => 500ms +#define NTP_RECEIVE_WAIT_TIME_MS 10 // 10ms + +typedef enum +{ + NTP_UPDATE_TIMEOUT = -1, + NTP_UPDATE_SUCCESS = 0, + NTP_UPDATE_DIFFTOOHIGH = 1, + NTP_UPDATE_TIME_INVALID = 2 +} ntp_return_values; + +/** + * @brief Own NTP Client library for Arduino with code from: + * - https://github.com/arduino-libraries/NTPClient + * - SPS&Technik - Projekt WordClock v1.02 + * + */ +class NTPClientPlus +{ +public: + NTPClientPlus(UDP &udp, const char *poolServerName, int utcx, bool _swChange); + void setupNTPClient(); + int updateNTP(); + void end(); + void setTimeOffset(int timeOffset); + void setPoolServerName(const char *poolServerName); + unsigned long getSecsSince1900() const; + unsigned long getEpochTime() const; + int getHours24() const; + int getHours12() const; + int getMinutes() const; + int getSeconds() const; + String getFormattedTime() const; + String getFormattedDate(); + void calcDate(); + unsigned int getDayOfWeek(); + unsigned int getYear(); + bool isLeapYear(unsigned int year); + int getMonth(int dayOfYear); + long getTimeOffset(); + bool updateSWChange(); + +private: + UDP *_udp; + bool _udpSetup = false; + + bool _swChange = 1; + const char *_poolServerName = "pool.ntp.org"; // Default time server + int _utcx = 0; + IPAddress _poolServerIP; + long _timeOffset = 0; + unsigned int _port = NTP_DEFAULT_LOCAL_PORT; + + unsigned long _updateInterval = 60000; // In ms + + unsigned long _currentEpoc = 0; // In s + unsigned long _lastUpdate = 0; // In ms + unsigned long _secsSince1900 = 0; // seconds since 1. Januar 1900, 00:00:00 + unsigned long _lastSecsSince1900 = 0; + unsigned int _dateYear = 0; + unsigned int _dateMonth = 0; + unsigned int _dateDay = 0; + unsigned int _dayOfWeek = 0; + + unsigned char _packetBuffer[NTP_PACKET_SIZE] = {0}; + void sendNTPPacket(); + void setSummertime(bool summertime); + + static const unsigned long secondperday = 86400; + static const unsigned long secondperhour = 3600; + static const unsigned long secondperminute = 60; + static const unsigned long minuteperhour = 60; + static const unsigned long millisecondpersecond = 1000; + + // number of days in months + unsigned int daysInMonth[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; +}; + +void wait(unsigned long time); + +#endif /* NTPCLIENTPLUS_H */ diff --git a/include/otafunctions.h b/include/otafunctions.h new file mode 100644 index 0000000..d0755e7 --- /dev/null +++ b/include/otafunctions.h @@ -0,0 +1,9 @@ +#ifndef OTAFUNCTIONS_H +#define OTAFUNCTIONS_H + +#include + +void handleOTA(); +void setupOTA(String hostname); + +#endif /* OTAFUNCTIONS_H */ diff --git a/include/own_font.h b/include/own_font.h new file mode 100644 index 0000000..c46d438 --- /dev/null +++ b/include/own_font.h @@ -0,0 +1,66 @@ +#ifndef OWNFONT_H +#define OWNFONT_H + +uint8_t numbers_font[10][5] = { {0b00000111, + 0b00000101, + 0b00000101, + 0b00000101, + 0b00000111}, + {0b00000001, + 0b00000001, + 0b00000001, + 0b00000001, + 0b00000001}, + {0b00000111, + 0b00000001, + 0b00000111, + 0b00000100, + 0b00000111}, + {0b00000111, + 0b00000001, + 0b00000111, + 0b00000001, + 0b00000111}, + {0b00000101, + 0b00000101, + 0b00000111, + 0b00000001, + 0b00000001}, + {0b00000111, + 0b00000100, + 0b00000111, + 0b00000001, + 0b00000111}, + {0b00000111, + 0b00000100, + 0b00000111, + 0b00000101, + 0b00000111}, + {0b00000111, + 0b00000001, + 0b00000001, + 0b00000001, + 0b00000001}, + {0b00000111, + 0b00000101, + 0b00000111, + 0b00000101, + 0b00000111}, + {0b00000111, + 0b00000101, + 0b00000111, + 0b00000001, + 0b00000111}}; + +uint8_t chars_font[2][5] = { {0b00000010, + 0b00000010, + 0b00000010, + 0b00000010, + 0b00000010}, + {0b00000111, + 0b00000101, + 0b00000111, + 0b00000100, + 0b00000100}}; + +#endif /* OWNFONT_H */ diff --git a/include/pong.h b/include/pong.h new file mode 100644 index 0000000..33710d9 --- /dev/null +++ b/include/pong.h @@ -0,0 +1,100 @@ +/** + * @file pong.h + * @author techniccontroller (mail[at]techniccontroller.com) + * @brief Class declaration for pong game + * @version 0.1 + * @date 2022-03-05 + * + * @copyright Copyright (c) 2022 + * + * main code from https://elektro.turanis.de/html/prj041/index.html + * + */ + +#ifndef PONG_H +#define PONG_H + +#include +#include "ledmatrix.h" +#include "udp_logger.h" + +#ifdef DEBOUNCE_TIME +#undef DEBOUNCE_TIME +#endif +#define DEBOUNCE_TIME 10 // in ms + +#define X_MAX 11 +#define Y_MAX 11 + +#ifdef GAME_DELAY +#undef GAME_DELAY +#endif +#define GAME_DELAY 80 // in ms + +#define BALL_DELAY_MAX 350 // in ms +#define BALL_DELAY_MIN 50 // in ms +#define BALL_DELAY_STEP 5 // in ms + +#define PLAYER_AMOUNT 2 +#define PLAYER_1 0 +#define PLAYER_2 1 + +#define PADDLE_WIDTH 3 + +#define PADDLE_MOVE_NONE 0 +#define PADDLE_MOVE_UP 1 +#define PADDLE_MOVE_DOWN 2 + +#ifdef LED_TYPE_OFF +#undef LED_TYPE_OFF +#endif +#define LED_TYPE_OFF 1 +#define LED_TYPE_PADDLE 2 +#define LED_TYPE_BALL 3 +#define LED_TYPE_BALL_RED 4 + +#define GAME_STATE_RUNNING 1 +#define GAME_STATE_END 2 +#define GAME_STATE_INIT 3 + +class Pong +{ + struct Coords + { + uint8_t x; + uint8_t y; + }; + +public: + Pong(); + Pong(LEDMatrix * matrix, UDPLogger * logger); + void loopCycle(); + void initGame(uint8_t numBots); + void ctrlUp(uint8_t playerid); + void ctrlDown(uint8_t playerid); + void ctrlNone(uint8_t playerid); + +private: + LEDMatrix *_ledmatrix; + UDPLogger *_logger; + uint8_t _gameState = 0; + uint8_t _numBots = 0; + uint8_t _playerMovement[PLAYER_AMOUNT] = {0}; + Coords _paddles[PLAYER_AMOUNT][PADDLE_WIDTH] = {0}; + Coords _ball = {0, 0}; + Coords _ball_old = {0, 0}; + int _ballMovement[2] = {0, 0}; + unsigned int _ballDelay = 0; + unsigned long _lastDrawUpdate = 0; + unsigned long _lastBallUpdate = 0; + unsigned long _lastButtonClick = 0; + + void updateBall(); + void endGame(); + void updateGame(); + uint8_t getPlayerMovement(uint8_t playerId); + void resetLEDs(); + void toggleLed(uint8_t x, uint8_t y, uint8_t type); +}; + +#endif /* PONG_H */ diff --git a/include/snake.h b/include/snake.h new file mode 100644 index 0000000..b6509b8 --- /dev/null +++ b/include/snake.h @@ -0,0 +1,94 @@ +/** + * @file snake.h + * @author techniccontroller (mail[at]techniccontroller.com) + * @brief Class declaration of snake game + * @version 0.1 + * @date 2022-03-05 + * + * @copyright Copyright (c) 2022 + * + * main code from https://elektro.turanis.de/html/prj099/index.html + * + */ +#ifndef snake_h +#define snake_h + +#include +#include "ledmatrix.h" +#include "udp_logger.h" + +#ifdef DEBOUNCE_TIME +#undef DEBOUNCE_TIME +#endif +#define DEBOUNCE_TIME 300 // in ms + +#define X_MAX 11 +#define Y_MAX 11 + +#ifdef GAME_DELAY +#undef GAME_DELAY +#endif +#define GAME_DELAY 400 // in ms + +#define LED_TYPE_SNAKE 1 +#ifdef LED_TYPE_OFF +#undef LED_TYPE_OFF +#endif +#define LED_TYPE_OFF 2 +#define LED_TYPE_FOOD 3 +#define LED_TYPE_BLOOD 4 + +#define DIRECTION_NONE 0 +#define DIRECTION_UP 1 +#define DIRECTION_DOWN 2 +#define DIRECTION_LEFT 3 +#define DIRECTION_RIGHT 4 + +#define GAME_STATE_RUNNING 1 +#define GAME_STATE_END 2 +#define GAME_STATE_INIT 3 + +#define MAX_TAIL_LENGTH (X_MAX * Y_MAX) +#define MIN_TAIL_LENGTH 3 + +class Snake +{ + + struct Coords + { + int x; + int y; + }; + +public: + Snake(); + Snake(LEDMatrix * matrix, UDPLogger * logger); + void loopCycle(); + void initGame(); + void ctrlUp(); + void ctrlDown(); + void ctrlLeft(); + void ctrlRight(); + +private: + LEDMatrix * _ledmatrix; + UDPLogger * _logger; + uint8_t _userDirection = 0; + uint8_t _gameState = 0; + Coords _head = {0, 0}; + Coords _tail[MAX_TAIL_LENGTH] = {0}; + Coords _food = {0, 0}; + unsigned long _lastDrawUpdate = 0; + unsigned long _lastButtonClick = 0; + unsigned int _wormLength = 0; + + void resetLEDs(); + void updateGame(); + void endGame(); + void updateTail(); + void updateFood(); + bool isCollision(); + void toggleLed(uint8_t x, uint8_t y, uint8_t type); +}; + +#endif \ No newline at end of file diff --git a/include/tetris.h b/include/tetris.h new file mode 100644 index 0000000..e6eb595 --- /dev/null +++ b/include/tetris.h @@ -0,0 +1,199 @@ +/** + * @file tetris.h + * @author techniccontroller (mail[at]techniccontroller.com) + * @brief Class definition for tetris game + * @version 0.1 + * @date 2022-03-05 + * + * @copyright Copyright (c) 2022 + * + * main tetris code originally written by Klaas De Craemer, Ing. David Hrbaty + * + */ +#ifndef TETRIS_H +#define TETRIS_H + +#include +#include "ledmatrix.h" +#include "udp_logger.h" + +#ifdef DEBOUNCE_TIME +#undef DEBOUNCE_TIME +#endif +#define DEBOUNCE_TIME 100 + +#define RED_END_TIME 1500 +#define GAME_STATE_RUNNING 1 +#define GAME_STATE_END 2 +#define GAME_STATE_INIT 3 +#define GAME_STATE_PAUSED 4 +#define GAME_STATE_READY 5 + +// common +#define DIR_UP 1 +#define DIR_DOWN 2 +#define DIR_LEFT 3 +#define DIR_RIGHT 4 +// Maximum size of bricks. Individual bricks can still be smaller (eg 3x3) +#define GREEN 0x008000 +#define RED 0xFF0000 +#define BLUE 0x0000FF +#define YELLOW 0xFFFF00 +#define CHOCOLATE 0xD2691E +#define PURPLE 0xFF00FF +#define WHITE 0XFFFFFF +#define AQUA 0x00FFFF +#define HOTPINK 0xFF1493 +#define DARKORANGE 0xFF8C00 + +#define MAX_BRICK_SIZE 4 +#define BRICKOFFSET -1 // Y offset for new bricks + +#define INIT_SPEED 800 // Initial delay in ms between brick drops +#define SPEED_STEP 10 // Factor for speed increase between levels, default 10 +#define LEVELUP 4 // Number of rows before levelup, default 5 + +#define WIDTH 11 +#define HEIGHT 11 + +class Tetris +{ + + // Playing field + struct Field + { + uint8_t pix[MATRIX_WIDTH][MATRIX_HEIGHT + 1]; // Make field one larger so that collision detection with bottom of field can be done in a uniform way + uint32_t color[MATRIX_WIDTH][MATRIX_HEIGHT]; + }; + + // Structure to represent active brick on screen + struct Brick + { + boolean enabled; // Brick is disabled when it has landed + int xpos, ypos; + + int yOffset; // Y-offset to use when placing brick at top of field + uint8_t siz; + uint8_t pix[MAX_BRICK_SIZE][MAX_BRICK_SIZE]; + + uint32_t col; + }; + + // Struct to contain the different choices of blocks + struct AbstractBrick + { + int yOffset; // Y-offset to use when placing brick at top of field + uint8_t siz; + uint8_t pix[MAX_BRICK_SIZE][MAX_BRICK_SIZE]; + uint32_t col; + }; + +public: + Tetris(); + Tetris(LEDMatrix *myledmatrix, UDPLogger *mylogger); + + void ctrlStart(); + void ctrlPlayPause(); + void ctrlRight(); + void ctrlLeft(); + void ctrlUp(); + void ctrlDown(); + void setSpeed(int32_t i); + + void loopCycle(); + +private: + void resetLEDs(); + void tetrisInit(); + void printField(); + + /* *** Game functions *** */ + void newActiveBrick(); + boolean checkFieldCollision(struct Brick *brick); + boolean checkSidesCollision(struct Brick *brick); + void rotateActiveBrick(); + void shiftActiveBrick(int dir); + void addActiveBrickToField(); + void moveFieldDownOne(uint8_t startRow); + void checkFullLines(); + + void clearField(); + void everythingRed(); + void showscore(); + + LEDMatrix *_ledmatrix; + UDPLogger *_logger; + Brick _activeBrick = {0}; + Field _field; + + bool _allowdrop = false; + bool _tetrisGameOver = false; + int _gameStatet = GAME_STATE_INIT; + int _score = 0; + unsigned int _speedtetris = 80; + unsigned long _brickSpeed = 0; + unsigned long _droptime = 0; + unsigned long _lastButtonClick = 0; + unsigned long _lastButtonClickr = 0; + unsigned long _nbRowsThisLevel = 0; + unsigned long _nbRowsTotal = 0; + unsigned long _prevUpdateTime = 0; + unsigned long _tetrisshowscore = 0; + + // color library + uint32_t _colorLib[10] = {RED, GREEN, BLUE, YELLOW, CHOCOLATE, PURPLE, WHITE, AQUA, HOTPINK, DARKORANGE}; + + // Brick "library" + AbstractBrick _brickLib[7] = { + {1, // yoffset when adding brick to field + 4, + {{0, 0, 0, 0}, + {0, 1, 1, 0}, + {0, 1, 1, 0}, + {0, 0, 0, 0}}, + WHITE}, + {0, + 4, + {{0, 1, 0, 0}, + {0, 1, 0, 0}, + {0, 1, 0, 0}, + {0, 1, 0, 0}}, + GREEN}, + {1, + 3, + {{0, 0, 0, 0}, + {1, 1, 1, 0}, + {0, 0, 1, 0}, + {0, 0, 0, 0}}, + BLUE}, + {1, + 3, + {{0, 0, 1, 0}, + {1, 1, 1, 0}, + {0, 0, 0, 0}, + {0, 0, 0, 0}}, + YELLOW}, + {1, + 3, + {{0, 0, 0, 0}, + {1, 1, 1, 0}, + {0, 1, 0, 0}, + {0, 0, 0, 0}}, + AQUA}, + {1, + 3, + {{0, 1, 1, 0}, + {1, 1, 0, 0}, + {0, 0, 0, 0}, + {0, 0, 0, 0}}, + HOTPINK}, + {1, + 3, + {{1, 1, 0, 0}, + {0, 1, 1, 0}, + {0, 0, 0, 0}, + {0, 0, 0, 0}}, + RED}}; +}; + +#endif /* TETRIS_H */ \ No newline at end of file diff --git a/include/udp_logger.h b/include/udp_logger.h new file mode 100644 index 0000000..4a929ea --- /dev/null +++ b/include/udp_logger.h @@ -0,0 +1,37 @@ +/** + * @file udp_logger.h + * @author techniccontroller (mail[at]techniccontroller.com) + * @brief Class for sending logging Strings as multicast messages + * @version 0.1 + * @date 2022-03-21 + * + * @copyright Copyright (c) 2022 + * + */ + +#ifndef UDP_LOGGER_H +#define UDP_LOGGER_H + +#include +#include + +class UDPLogger +{ +public: + UDPLogger(); + UDPLogger(IPAddress interface_addr, IPAddress multicast_addr, int port, String name); + void set_name(String name); + void log_string(String message); + void log_color_24bit(uint32_t color); + +private: + char _packetBuffer[100] = {0}; + int _port; + IPAddress _interfaceAddr; + IPAddress _multicastAddr; + String _name = "Logger"; + unsigned long _lastSend = 0; + WiFiUDP _udp; +}; + +#endif /* UDP_LOGGER_H */ \ No newline at end of file diff --git a/include/wordclock_constants.h b/include/wordclock_constants.h new file mode 100644 index 0000000..0f3c6e7 --- /dev/null +++ b/include/wordclock_constants.h @@ -0,0 +1,71 @@ +#ifndef WORDCLOCK_CONSTANTS_H +#define WORDCLOCK_CONSTANTS_H + +#include + +// ---------------------------------------------------------------------------------- +// CONSTANTS +// ---------------------------------------------------------------------------------- +#define AP_SSID "WordclockAP" // SSID name of Access Point +#define NTP_SERVER_URL "de.pool.ntp.org" // NTP server address +#define HOSTNAME (String("wordclock")) // Local hostname +#define LOGGER_MULTICAST_IP (IPAddress(230, 120, 10, 2)) // IP for UDP server +#define LOGGER_MULTICAST_PORT 8123 // Port for UDP server +#define HTTP_PORT 80 // Standard HTTP port + +// EEPROM layout +typedef enum +{ + ADR_NM_START_H = 0, + ADR_NM_END_H = 4, + ADR_NM_START_M = 8, + ADR_NM_END_M = 12, + ADR_BRIGHTNESS = 16, + ADR_MC_RED = 20, + ADR_MC_GREEN = 22, + ADR_MC_BLUE = 24, + EEPROM_SIZE = 30 +} EepromLayout_en; + +#define NEOPIXEL_PIN 5 // pin to which the NeoPixels are attached +#define BUTTON_PIN 14 // pin to which the button is attached + +// Night mode +#define NIGHTMODE_START_HR 23 +#define NIGHTMODE_START_MIN 0 +#define NIGHTMODE_END_HR 7 +#define NIGHTMODE_END_MIN 0 + +// Timings +#define PERIOD_ANIMATION 200 +#define PERIOD_HEARTBEAT 1000 +#define PERIOD_MATRIX_UPDATE 100 +#define PERIOD_NIGHTMODE_CHECK 20000 +#define PERIOD_NTP_UPDATE 30000 +#define PERIOD_PONG 10 +#define PERIOD_SNAKE 50 +#define PERIOD_STATE_CHANGE 10000 +#define PERIOD_TETRIS 50 +#define PERIOD_TIME_VISU_UPDATE 1000 +#define TIMEOUT_LEDDIRECT 5000 + +#define SHORT_PRESS_MS 100 +#define LONG_PRESS_MS 2000 + +// Current limit +#define CURRENT_LIMIT_LED 2500 // limit the total current consumed by LEDs (mA) + +// LED smoothing +#define DEFAULT_SMOOTHING_FACTOR 0.5 + +// Number of colors in colors array +#define NUM_COLORS 7 + +// LED matrix size +#define MATRIX_WIDTH 11 +#define MATRIX_HEIGHT 11 + +// State machine states count +#define NUM_STATES 6 + +#endif /* WORDCLOCK_CONSTANTS_H */ diff --git a/include/wordclock_esp8266.h b/include/wordclock_esp8266.h new file mode 100644 index 0000000..cde48a3 --- /dev/null +++ b/include/wordclock_esp8266.h @@ -0,0 +1,51 @@ +#ifndef WORDCLOCK_ESP8266_H +#define WORDCLOCK_ESP8266_H + +#include +#include +#include "ledmatrix.h" +#include "udp_logger.h" + +extern UDPLogger logger; +extern LEDMatrix led_matrix; +extern ESP8266WebServer webserver; + +typedef struct +{ + int nightmode_start_hour; + int nightmode_start_min; + int nightmode_end_hour; + int nightmode_end_min; +} NightModeTimes_st; + +typedef enum +{ + ST_CLOCK, + ST_DICLOCK, + ST_SPIRAL, + ST_TETRIS, + ST_SNAKE, + ST_PINGPONG +} clock_state_en; + +int EEPROM_read_address(int address); +String leading_zero2digit(int value); +String split(String s, char parser, int index); +void check_night_mode(void); +void EEPROM_write_to_address(int address, int value); +void handle_button(void); +void handle_command(void); +void handle_current_state(void); +void handle_data_request(void); +void handle_led_direct(void); +void load_main_color(void); +void ntp_time_update(void); +void on_state_entry(uint8_t state); +void send_heartbeat(void); +void set_main_color(uint8_t red, uint8_t green, uint8_t blue); +void set_night_mode(bool on); +void state_change(uint8_t newState); +void update_matrix(void); +void update_state_machine(void); + +#endif /* WORDCLOCK_ESP8266_H */ diff --git a/include/wordclock_functions.h b/include/wordclock_functions.h new file mode 100644 index 0000000..cabd1dc --- /dev/null +++ b/include/wordclock_functions.h @@ -0,0 +1,10 @@ +#ifndef WORDCLOCK_FUNCTIONS_H +#define WORDCLOCK_FUNCTIONS_H + +#include + +int show_string_on_clock(String message, uint32_t color); +String time_to_string(uint8_t hours, uint8_t minutes); +void draw_minute_indicator(uint8_t minutes, uint32_t color); + +#endif /* WORDCLOCK_FUNCTIONS_H */ diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..5519f91 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,20 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:nodemcuv2] +platform = espressif8266 +board = nodemcuv2 +framework = arduino +lib_deps = + adafruit/Adafruit NeoMatrix@^1.3.0 + adafruit/Adafruit NeoPixel@^1.11.0 + tzapu/WiFiManager@^0.16.0 +monitor_speed = 115200 +monitor_filters = direct diff --git a/scripts/multicastUDP_receiver.py b/scripts/multicastUDP_receiver.py new file mode 100644 index 0000000..98ac00e --- /dev/null +++ b/scripts/multicastUDP_receiver.py @@ -0,0 +1,59 @@ +import socket +import struct +import sys +from datetime import datetime +import queue + +# ip address of network interface +MCAST_IF_IP = '192.168.178.66' + +multicast_group = '230.120.10.2' +server_address = ('', 8123) + +# Create the socket +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + +# Bind to the server address +sock.bind(server_address) + +print("Start") + +# Tell the operating system to add the socket to the multicast group +# on all interfaces. +group = socket.inet_aton(multicast_group) +mreq = struct.pack('4s4s', group, socket.inet_aton(MCAST_IF_IP)) +sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) + +print("Ready") +saveCounter = 0 + +buffer = queue.Queue(20) + +# Receive/respond loop +while True: + data, address = sock.recvfrom(1024) + data_str = data.decode("utf-8").strip() + print(address, ": ", data_str) + data_str = datetime.now().strftime('%b-%d-%Y_%H%M%S') + ": " + data_str + buffer.put(data_str) + if buffer.full(): + buffer.get() + + + if "NTP-Update not successful" in data_str or "Start program" in data_str: + f = open("log.txt",'a') + while not buffer.empty(): + f.write(buffer.get()) + f.write("\n") + f.close() + saveCounter = 20 + + if saveCounter > 0: + f = open("log.txt",'a') + f.write(data_str) + f.write("\n") + if saveCounter == 1: + f.write("\n") + f.close() + saveCounter -= 1 + diff --git a/src/connectivity/ntp_client_plus.cpp b/src/connectivity/ntp_client_plus.cpp new file mode 100644 index 0000000..3e5ca9d --- /dev/null +++ b/src/connectivity/ntp_client_plus.cpp @@ -0,0 +1,693 @@ +#include +#include "ntp_client_plus.h" + +/** + * @brief Construct a new NTPClientPlus::NTPClientPlus object + * + * @param udp UDP client + * @param poolServerName time server name + * @param utcx UTC offset (in 1h) + * @param _swChange should summer/winter time be considered + */ +NTPClientPlus::NTPClientPlus(UDP &udp, const char *poolServerName, int utcx, bool _swChange) +{ + this->_udp = &udp; + this->_utcx = utcx; + this->_timeOffset = this->secondperhour * this->_utcx; + this->_poolServerName = poolServerName; + this->_swChange = _swChange; +} + +/** + * @brief Starts the underlying UDP client, get first NTP timestamp and calc date + * + */ +void NTPClientPlus::setupNTPClient() +{ + this->_udp->begin(this->_port); + this->_udpSetup = true; + this->updateNTP(); + this->calcDate(); +} + +/** + * @brief Get new update from NTP + * + * @return NTP_UPDATE_TIMEOUT timeout after 500 ms + * @return NTP_UPDATE_SUCCESS after successful update + * @return NTP_UPDATE_DIFFTOOHIGH too much difference to previous received time (try again) + * @return NTP_UPDATE_TIME_INVALID time value is invalid + */ +int NTPClientPlus::updateNTP() +{ + + // flush any existing packets + while (this->_udp->parsePacket() != 0) + { + this->_udp->flush(); + } + + this->sendNTPPacket(); + + // Wait till data is there or timeout... + uint8_t conn_tries = 0; + int received_bytes = 0; + + while ((received_bytes == 0) && (conn_tries++ <= MAX_NTP_CONN_TRIES)) + { + received_bytes = this->_udp->parsePacket(); + wait(NTP_RECEIVE_WAIT_TIME_MS); + } + + if (conn_tries >= MAX_NTP_CONN_TRIES) + { + return NTP_UPDATE_TIMEOUT; + } + + this->_udp->read(this->_packetBuffer, NTP_PACKET_SIZE); + + unsigned long highWord = word(this->_packetBuffer[40], this->_packetBuffer[41]); + unsigned long lowWord = word(this->_packetBuffer[42], this->_packetBuffer[43]); + // combine the four bytes (two words) into a long integer + // this is NTP time (seconds since Jan 1 1900): + unsigned long tempSecsSince1900 = highWord << 16 | lowWord; + + if (tempSecsSince1900 < UNIX_TIMESTAMP_1900) // NTP time is not valid + { + return NTP_UPDATE_TIME_INVALID; + } + + // check if time off last ntp update is roughly in the same range: 100sec apart (validation check) + if (this->_lastSecsSince1900 == 0 || tempSecsSince1900 - this->_lastSecsSince1900 < 100000) + { + // Only update time then + this->_lastUpdate = millis() - (NTP_RECEIVE_WAIT_TIME_MS * (conn_tries + 1)); // Account for delay in reading the time + this->_secsSince1900 = tempSecsSince1900; + this->_currentEpoc = this->_secsSince1900 - UNIX_TIMESTAMP_1900; + + // Remember time of last update + this->_lastSecsSince1900 = tempSecsSince1900; + return NTP_UPDATE_SUCCESS; // return 0 after successful update + } + else + { + // Remember time of last update + this->_lastSecsSince1900 = tempSecsSince1900; + return NTP_UPDATE_DIFFTOOHIGH; + } +} + +/** + * @brief Stops the underlying UDP client + * + */ +void NTPClientPlus::end() +{ + this->_udp->stop(); + this->_udpSetup = false; +} + +/** + * @brief Setter TimeOffset + * + * @param timeOffset offset from UTC in seconds + */ +void NTPClientPlus::setTimeOffset(int timeOffset) +{ + this->_timeOffset = timeOffset; +} + +long NTPClientPlus::getTimeOffset() +{ + return this->_timeOffset; +} + +/** + * @brief Set time server name + * + * @param poolServerName + */ +void NTPClientPlus::setPoolServerName(const char *poolServerName) +{ + this->_poolServerName = poolServerName; +} + +/** + * @brief Calc seconds since 1. Jan. 1900 + * + * @return unsigned long seconds since 1. Jan. 1900 + */ +unsigned long NTPClientPlus::getSecsSince1900() const +{ + return this->_timeOffset + // User offset + this->_secsSince1900 + // seconds returned by the NTP server + ((millis() - this->_lastUpdate) / 1000); // Time since last update +} + +/** + * @brief Get UNIX Epoch time since 1. Jan. 1970 + * + * @return unsigned long UNIX Epoch time since 1. Jan. 1970 in seconds + */ +unsigned long NTPClientPlus::getEpochTime() const +{ + return this->getSecsSince1900() - UNIX_TIMESTAMP_1900; +} + +/** + * @brief Get current hours in 24h format + * + * @return int + */ +int NTPClientPlus::getHours24() const +{ + int hours = ((this->getEpochTime() % 86400L) / 3600); + return hours; +} + +/** + * @brief Get current hours in 12h format + * + * @return int + */ +int NTPClientPlus::getHours12() const +{ + int hours = this->getHours24(); + if (hours >= 12) + { + hours = hours - 12; + } + return hours; +} + +/** + * @brief Get current minutes + * + * @return int + */ +int NTPClientPlus::getMinutes() const +{ + return ((this->getEpochTime() % 3600) / 60); +} + +/** + * @brief Get current seconds + * + * @return int + */ +int NTPClientPlus::getSeconds() const +{ + return (this->getEpochTime() % 60); +} + +/** + * @brief + * + * @return String time formatted like `hh:mm:ss` + */ +String NTPClientPlus::getFormattedTime() const +{ + unsigned long rawTime = this->getEpochTime(); + unsigned long hours = (rawTime % 86400L) / 3600; + String hoursStr = hours < 10 ? "0" + String(hours) : String(hours); + + unsigned long minutes = (rawTime % 3600) / 60; + String minuteStr = minutes < 10 ? "0" + String(minutes) : String(minutes); + + unsigned long seconds = rawTime % 60; + String secondStr = seconds < 10 ? "0" + String(seconds) : String(seconds); + + return hoursStr + ":" + minuteStr + ":" + secondStr; +} + +/** + * @brief + * + * @return String date formatted like `dd.mm.yyyy` + */ +String NTPClientPlus::getFormattedDate() +{ + this->calcDate(); + unsigned int dateDay = this->_dateDay; + unsigned int dateMonth = this->_dateMonth; + unsigned int dateYear = this->_dateYear; + + String dayStr = dateDay < 10 ? "0" + String(dateDay) : String(dateDay); + String monthStr = dateMonth < 10 ? "0" + String(dateMonth) : String(dateMonth); + String yearStr = dateYear < 10 ? "0" + String(dateYear) : String(dateYear); + + return dayStr + "." + monthStr + "." + yearStr; +} + +/** + * @brief Calc date from seconds since 1900 + * + */ +void NTPClientPlus::calcDate() +{ + // Start: Calc date + + // get days since 1900 + unsigned long days1900 = this->getSecsSince1900() / secondperday; + + // calc current year + this->_dateYear = this->getYear(); + + // calc how many leap days since 1.Jan 1900 + int leapDays = 0; + for (unsigned int i = 1900; i < this->_dateYear; i++) + { + // check if leap year + if (this->isLeapYear(i)) + { + leapDays++; + } + } + leapDays = leapDays - 1; + + // check if current year is leap year + if (this->isLeapYear(this->_dateYear)) + { + daysInMonth[2] = 29; + } + else + { + daysInMonth[2] = 28; + } + + unsigned int dayOfYear = (days1900 - ((this->_dateYear - 1900) * 365) - leapDays); + + // calc current month + this->_dateMonth = this->getMonth(dayOfYear); + + this->_dateDay = 0; + + // calc day of month + for (unsigned int i = 0; i < this->_dateMonth; i++) + { + this->_dateDay = this->_dateDay + daysInMonth[i]; + } + this->_dateDay = dayOfYear - this->_dateDay; + + // calc day of week: + // Monday = 1, Tuesday = 2, Wednesday = 3, Thursday = 4, Friday = 5, Saturday = 6, Sunday = 7 + // 1. Januar 1900 was a monday + this->_dayOfWeek = 1; + + for (unsigned int i = 0; i < days1900; i++) + { + if (this->_dayOfWeek < 7) + { + this->_dayOfWeek = this->_dayOfWeek + 1; + } + else + { + this->_dayOfWeek = 1; + } + } + + // End: Calc date (dateDay, dateMonth, dateYear) + + // calc if summer time active + + this->updateSWChange(); +} + +/** + * @brief Getter for day of the week + * + * @return unsigned int + */ +unsigned int NTPClientPlus::getDayOfWeek() +{ + return this->_dayOfWeek; +} + +/** + * @brief Function to calc current year + * + * @return unsigned int + */ +unsigned int NTPClientPlus::getYear() +{ + + unsigned long sec1900 = this->getSecsSince1900(); + + // NTP starts at 1. Jan 1900 + unsigned int result = 1900; + unsigned int dayInYear = 0; + unsigned int days = 0; + unsigned int days1900 = 0; + + unsigned int for_i = 0; + bool leapYear = LOW; + + days1900 = sec1900 / this->secondperday; + + for (for_i = 0; for_i < days1900; for_i++) + { + + leapYear = this->isLeapYear(result); + + if (leapYear) + { + dayInYear = 366; + } + + else + { + dayInYear = 365; + } + + days++; + + if (days >= dayInYear) + { + result++; + days = 0; + } + } + + return result; +} + +/** + * @brief Function to check if given year is leap year + * + * @param year + * @return true + * @return false + */ +bool NTPClientPlus::isLeapYear(unsigned int year) +{ + + bool result = LOW; + + // check for leap year + if ((year % 4) == 0) + { + + result = HIGH; + + if ((year % 100) == 0) + { + + result = LOW; + + if ((year % 400) == 0) + { + + result = HIGH; + } + } + } + + else + { + result = LOW; + } + + return result; +} + +/** + * @brief Get Month of given day of year + * + * @param dayOfYear + * @return int + */ +int NTPClientPlus::getMonth(int dayOfYear) +{ + + bool leapYear = this->isLeapYear(this->getYear()); + + // Month beginnings + int monthMin[13] = {0, 1, 32, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335}; + // Month endings + int monthMax[13] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365}; + + int month = 0; + + int y = 0; + + // Calculation of the beginning and end of each month in the leap year + if (leapYear == HIGH) + { + + for (y = 3; y < 13; y++) + { + monthMin[y] = monthMin[y] + 1; + } + + for (y = 2; y < 13; y++) + { + monthMax[y] = monthMax[y] + 1; + } + } + + // January + if (dayOfYear >= monthMin[1] && dayOfYear <= monthMax[1]) + { + month = 1; + } + + // February + if (dayOfYear >= monthMin[2] && dayOfYear <= monthMax[2]) + { + month = 2; + } + + // March + if (dayOfYear >= monthMin[3] && dayOfYear <= monthMax[3]) + { + month = 3; + } + + // April + if (dayOfYear >= monthMin[4] && dayOfYear <= monthMax[4]) + { + month = 4; + } + + // May + if (dayOfYear >= monthMin[5] && dayOfYear <= monthMax[5]) + { + month = 5; + } + + // June + if (dayOfYear >= monthMin[6] && dayOfYear <= monthMax[6]) + { + month = 6; + } + + // July + if (dayOfYear >= monthMin[7] && dayOfYear <= monthMax[7]) + { + month = 7; + } + + // August + if (dayOfYear >= monthMin[8] && dayOfYear <= monthMax[8]) + { + month = 8; + } + + // September + if (dayOfYear >= monthMin[9] && dayOfYear <= monthMax[9]) + { + month = 9; + } + + // October + if (dayOfYear >= monthMin[10] && dayOfYear <= monthMax[10]) + { + month = 10; + } + + // November + if (dayOfYear >= monthMin[11] && dayOfYear <= monthMax[11]) + { + month = 11; + } + + // December + if (dayOfYear >= monthMin[12] && dayOfYear <= monthMax[12]) + { + month = 12; + } + + return month; +} + +/** + * @brief (private) Send NTP Packet to NTP server + * + */ +void NTPClientPlus::sendNTPPacket() +{ + // set all bytes in the buffer to 0 + memset(this->_packetBuffer, 0, NTP_PACKET_SIZE); + // Initialize values needed to form NTP request + this->_packetBuffer[0] = 0b11100011; // LI, Version, Mode + this->_packetBuffer[1] = 0; // Stratum, or type of clock + this->_packetBuffer[2] = 6; // Polling Interval + this->_packetBuffer[3] = 0xEC; // Peer Clock Precision + // 8 bytes of zero for Root Delay & Root Dispersion + this->_packetBuffer[12] = 49; + this->_packetBuffer[13] = 0x4E; + this->_packetBuffer[14] = 49; + this->_packetBuffer[15] = 52; + + // all NTP fields have been given values, now + // you can send a packet requesting a timestamp: + if (this->_poolServerName) + { + this->_udp->beginPacket(this->_poolServerName, 123); + } + else + { + this->_udp->beginPacket(this->_poolServerIP, 123); + } + this->_udp->write(this->_packetBuffer, NTP_PACKET_SIZE); + this->_udp->endPacket(); +} + +/** + * @brief (private) Set time offset accordance to summer time + * + * @param summertime + */ +void NTPClientPlus::setSummertime(bool summertime) +{ + if (summertime) + { + this->_timeOffset = this->secondperhour * (this->_utcx + 1); + } + else + { + this->_timeOffset = this->secondperhour * (this->_utcx); + } +} + +/** + * @brief (private) Update Summer/Winter time change + * + * @returns bool summertime active + */ +bool NTPClientPlus::updateSWChange() +{ + unsigned int dayOfWeek = this->_dayOfWeek; + unsigned int dateDay = this->_dateDay; + unsigned int dateMonth = this->_dateMonth; + + bool summertimeActive = false; + + if (this->_swChange) + { + // Start: Set summer-/ winter time + + // current month is march + if (dateMonth == 3) + { + + // it is last week in march + if ((this->daysInMonth[3] - dateDay) < 7) + { + + // Example year 2020: March 31 days; Restart March 26, 2020 (Thursday = weekday = 4); 5 days remaining; Last Sunday March 29, 2020 + // Calculation: 31 - 26 = 5; 5 + 4 = 9; + // Result: Last day in March is a Tuesday. There follows another Sunday in October => set winter time + + // Example year 2021: March 31 days; Restart March 30, 2021 (Tuesday = weekday = 2); 1 days remaining; Last Sunday March 28, 2021 + // Calculation: 31 - 30 = 1; 1 + 2 = 3; + // Result: Last day in March is a Wednesday. Changeover to summer time already done => set summer time + + // There follows within the last week in March one more Sunday => set winter time + if (((this->daysInMonth[3] - dateDay) + dayOfWeek) >= 7) + { + this->setSummertime(0); + summertimeActive = false; + } + + // last sunday in march already over -> summer time + else + { + this->setSummertime(1); + summertimeActive = true; + } + } + + // restart in first three weeks of march -> winter time + else + { + this->setSummertime(0); + summertimeActive = false; + } + } + + // current month is october + else if (dateMonth == 10) + { + + // restart last week of october + if ((this->daysInMonth[10] - dateDay) < 7) + { + + // Example year 2020: October 31 days; restart October 26, 2020 (Monday = weekday = 1); 5 days remaining; last Sunday October 25, 2020 + // Calculation: 31 - 26 = 5; 5 + 1 = 6; + // Result: Last day in October is a Saturday. Changeover to winter time already done => set winter time + + // Example year 2021: October 31 days; Restart 26. October 2021 (Tuesday = weekday = 2); 5 days remaining; Last Sunday 31. October 2021 + // Calculation: 31 - 26 = 5; 5 + 2 = 7; + // Result: Last day in October is a Sunday. There follows another Sunday in October => set summer time + + // There follows within the last week in October one more Sunday => summer time + if (((this->daysInMonth[10] - dateDay) + dayOfWeek) >= 7) + { + this->setSummertime(1); + summertimeActive = true; + } + + // last sunday in october already over -> winter time + else + { + this->setSummertime(0); + summertimeActive = false; + } + } + + // restart in first three weeks of october -> summer time + else + { + this->setSummertime(1); + summertimeActive = true; + } + } + + // restart in summer time + else if (dateMonth > 3 && dateMonth < 10) + { + this->setSummertime(1); + summertimeActive = true; + } + + // restart in winter time + else if (dateMonth < 3 || dateMonth > 10) + { + this->setSummertime(0); + summertimeActive = false; + } + } + + return summertimeActive; +} + +void wait(unsigned long time) +{ + unsigned long start = millis(); + while (millis() - start < time) + { + yield(); + }; +} diff --git a/src/connectivity/otafunctions.cpp b/src/connectivity/otafunctions.cpp new file mode 100644 index 0000000..10dcda7 --- /dev/null +++ b/src/connectivity/otafunctions.cpp @@ -0,0 +1,65 @@ +#include +#include +#include "otafunctions.h" + +// setup Arduino OTA +void setupOTA(String hostname) +{ + // Port defaults to 8266 + // ArduinoOTA.setPort(8266); + + // Hostname defaults to esp8266-[ChipID] + ArduinoOTA.setHostname(hostname.c_str()); + + // No authentication by default + // ArduinoOTA.setPassword("admin"); + + // Password can be set with it's md5 value as well + // MD5(admin) = 21232f297a57a5a743894a0e4a801fc3 + // ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3"); + + ArduinoOTA.onStart([]() + { + String type; + if (ArduinoOTA.getCommand() == U_FLASH) + { + type = "sketch"; + } + else + { // U_FS + type = "filesystem"; + } + + // NOTE: if updating FS this would be the place to unmount FS using FS.end() + // Serial.println("Start updating " + type); + }); + ArduinoOTA.onEnd([]() + { + // Serial.println("\nEnd"); + }); + ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) + { + // Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + }); + ArduinoOTA.onError([](ota_error_t error) + { + //Serial.printf("Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) { + //Serial.println("Auth Failed"); + } else if (error == OTA_BEGIN_ERROR) { + //Serial.println("Begin Failed"); + } else if (error == OTA_CONNECT_ERROR) { + //Serial.println("Connect Failed"); + } else if (error == OTA_RECEIVE_ERROR) { + //Serial.println("Receive Failed"); + } else if (error == OTA_END_ERROR) { + //Serial.println("End Failed"); + } }); + ArduinoOTA.begin(); +} + +void handleOTA() +{ + // handle OTA + ArduinoOTA.handle(); +} \ No newline at end of file diff --git a/src/connectivity/udp_logger.cpp b/src/connectivity/udp_logger.cpp new file mode 100644 index 0000000..3e514d5 --- /dev/null +++ b/src/connectivity/udp_logger.cpp @@ -0,0 +1,43 @@ +#include "udp_logger.h" + +UDPLogger::UDPLogger() +{ +} + +UDPLogger::UDPLogger(IPAddress interface_addr, IPAddress multicast_addr, int port, String name) +{ + _interfaceAddr = interface_addr; + _multicastAddr = multicast_addr; + _name = name; + _port = port; + _udp.beginMulticast(_interfaceAddr, _multicastAddr, _port); +} + +void UDPLogger::set_name(String name) +{ + _name = name; +} + +void UDPLogger::log_string(String message) +{ + // wait 5 milliseconds if last send was less than 5 milliseconds before + if (millis() < (_lastSend + 5)) + { + delay(5); + } + message = _name + ": " + message; + Serial.println(message); + _udp.beginPacketMulticast(_multicastAddr, _port, _interfaceAddr); + message.toCharArray(_packetBuffer, 100); + _udp.print(_packetBuffer); + _udp.endPacket(); + _lastSend = millis(); +} + +void UDPLogger::log_color_24bit(uint32_t color) +{ + uint8_t result_red = color >> 16 & 0xff; + uint8_t result_green = color >> 8 & 0xff; + uint8_t result_blue = color & 0xff; + log_string(String(result_red) + ", " + String(result_green) + ", " + String(result_blue)); +} \ No newline at end of file diff --git a/src/games/pong.cpp b/src/games/pong.cpp new file mode 100644 index 0000000..54e3234 --- /dev/null +++ b/src/games/pong.cpp @@ -0,0 +1,332 @@ +/** + * @file pong.cpp + * @author techniccontroller (mail[at]techniccontroller.com) + * @brief Class implementation for pong game + * @version 0.1 + * @date 2022-03-06 + * + * @copyright Copyright (c) 2022 + * + * main code from https://elektro.turanis.de/html/prj041/index.html + * + */ +#include "pong.h" + +/** + * @brief Construct a new Pong:: Pong object + * + */ +Pong::Pong() +{ +} + +/** + * @brief Construct a new Pong:: Pong object + * + * @param myledmatrix pointer to LEDMatrix object, need to provide gridAddPixel(x, y, col), gridFlush() + * @param mylogger pointer to UDPLogger object, need to provide a function log_string(message) + */ +Pong::Pong(LEDMatrix * matrix, UDPLogger * logger) +{ + _ledmatrix = matrix; + _logger = logger; + _gameState = GAME_STATE_END; +} + +/** + * @brief Run main loop for one cycle + * + */ +void Pong::loopCycle() +{ + switch (_gameState) + { + case GAME_STATE_INIT: + initGame(2); + break; + case GAME_STATE_RUNNING: + updateBall(); + updateGame(); + break; + case GAME_STATE_END: + break; + } +} + +/** + * @brief Trigger control: UP for given player + * + * @param playerid id of player {0, 1} + */ +void Pong::ctrlUp(uint8_t playerid) +{ + if (millis() > _lastButtonClick + DEBOUNCE_TIME) + { + _playerMovement[playerid] = PADDLE_MOVE_DOWN; // need to swap direction as field is rotated 180deg + _lastButtonClick = millis(); + } +} + +/** + * @brief Trigger control: DOWN for given player + * + * @param playerid id of player {0, 1} + */ +void Pong::ctrlDown(uint8_t playerid) +{ + if (millis() > _lastButtonClick + DEBOUNCE_TIME) + { + _playerMovement[playerid] = PADDLE_MOVE_UP; // need to swap direction as field is rotated 180deg + _lastButtonClick = millis(); + } +} + +/** + * @brief Trigger control: NONE for given player + * + * @param playerid id of player {0, 1} + */ +void Pong::ctrlNone(uint8_t playerid) +{ + if (millis() > _lastButtonClick + DEBOUNCE_TIME) + { + _playerMovement[playerid] = PADDLE_MOVE_NONE; + _lastButtonClick = millis(); + } +} + +/** + * @brief Initialize a new game + * + * @param numBots number of bots {0, 1, 2} -> two bots results in animation + */ +void Pong::initGame(uint8_t numBots) +{ + (*_logger).log_string("Pong: init with " + String(numBots) + " Bots"); + resetLEDs(); + _lastButtonClick = millis(); + + _numBots = numBots; + + _ball.x = 1; + _ball.y = (Y_MAX / 2) - (PADDLE_WIDTH / 2) + 1; + _ball_old.x = _ball.x; + _ball_old.y = _ball.y; + _ballMovement[0] = 1; + _ballMovement[1] = -1; + _ballDelay = BALL_DELAY_MAX; + + for (uint8_t i = 0; i < PADDLE_WIDTH; i++) + { + _paddles[PLAYER_1][i].x = 0; + _paddles[PLAYER_1][i].y = (Y_MAX / 2) - (PADDLE_WIDTH / 2) + i; + _paddles[PLAYER_2][i].x = X_MAX - 1; + _paddles[PLAYER_2][i].y = _paddles[PLAYER_1][i].y; + } + + _gameState = GAME_STATE_RUNNING; +} + +/** + * @brief Update ball position + * + */ +void Pong::updateBall() +{ + bool hitBall = false; + if ((millis() - _lastBallUpdate) < _ballDelay) + { + return; + } + _lastBallUpdate = millis(); + toggleLed(_ball.x, _ball.y, LED_TYPE_OFF); + + // collision detection for player 1 + if (_ballMovement[0] == -1 && _ball.x == 1) + { + for (uint8_t i = 0; i < PADDLE_WIDTH; i++) + { + if (_paddles[PLAYER_1][i].y == _ball.y) + { + hitBall = true; + break; + } + } + } + + // collision detection for player 2 + if (_ballMovement[0] == 1 && _ball.x == X_MAX - 2) + { + for (uint8_t i = 0; i < PADDLE_WIDTH; i++) + { + if (_paddles[PLAYER_2][i].y == _ball.y) + { + hitBall = true; + break; + } + } + } + + if (hitBall == true) + { + _ballMovement[0] *= -1; + if (_ballDelay > BALL_DELAY_MIN) + { + _ballDelay -= BALL_DELAY_STEP; + } + } + + _ball.x += _ballMovement[0]; + _ball.y += _ballMovement[1]; + + if (_ball.x <= 0 || _ball.x >= X_MAX - 1) + { + endGame(); + return; + } + + if (_ball.y <= 0 || _ball.y >= Y_MAX - 1) + { + _ballMovement[1] *= -1; + } + + toggleLed(_ball.x, _ball.y, LED_TYPE_BALL); +} + +/** + * @brief Game over, draw ball red + * + */ +void Pong::endGame() +{ + (*_logger).log_string("Pong: Game ended"); + _gameState = GAME_STATE_END; + toggleLed(_ball.x, _ball.y, LED_TYPE_BALL_RED); +} + +/** + * @brief Update paddle position and check for game over + * + */ +void Pong::updateGame() +{ + if ((millis() - _lastDrawUpdate) < GAME_DELAY) + { + return; + } + _lastDrawUpdate = millis(); + + // turn off paddle LEDs + for (uint8_t p = 0; p < PLAYER_AMOUNT; p++) + { + for (uint8_t i = 0; i < PADDLE_WIDTH; i++) + { + toggleLed(_paddles[p][i].x, _paddles[p][i].y, LED_TYPE_OFF); + } + } + + // move _paddles + for (uint8_t p = 0; p < PLAYER_AMOUNT; p++) + { + uint8_t movement = getPlayerMovement(p); + if (movement == PADDLE_MOVE_UP && _paddles[p][PADDLE_WIDTH - 1].y < (Y_MAX - 1)) + { + for (uint8_t i = 0; i < PADDLE_WIDTH; i++) + { + _paddles[p][i].y++; + } + } + if (movement == PADDLE_MOVE_DOWN && _paddles[p][0].y > 0) + { + for (uint8_t i = 0; i < PADDLE_WIDTH; i++) + { + _paddles[p][i].y--; + } + } + } + + // show paddle LEDs + for (uint8_t p = 0; p < PLAYER_AMOUNT; p++) + { + for (uint8_t i = 0; i < PADDLE_WIDTH; i++) + { + toggleLed(_paddles[p][i].x, _paddles[p][i].y, LED_TYPE_PADDLE); + } + } +} + +/** + * @brief Get the next movement of paddle from given player + * + * @param playerId id of player {0, 1} + * @return uint8_t movement {UP, DOWN, NONE} + */ +uint8_t Pong::getPlayerMovement(uint8_t playerId) +{ + uint8_t action = PADDLE_MOVE_NONE; + if (playerId < _numBots) + { + // bot moves paddle + int8_t ydir = _ball_old.y - _ball.y; + int8_t diff = _paddles[playerId][PADDLE_WIDTH / 2].y - _ball.y + ydir * 0.5; + // no movement if ball moves away from paddle or no difference between ball and paddle + if (diff == 0 || (_ballMovement[0] > 0 && playerId == 0) || (_ballMovement[0] < 0 && playerId == 1)) + { + action = PADDLE_MOVE_NONE; + } + else if (diff > 0) + { + action = PADDLE_MOVE_DOWN; + } + else + { + action = PADDLE_MOVE_UP; + } + } + else + { + action = _playerMovement[playerId]; + _playerMovement[playerId] = PADDLE_MOVE_NONE; + } + return action; +} + +/** + * @brief Clear the led matrix (turn all leds off) + * + */ +void Pong::resetLEDs() +{ + (*_ledmatrix).flush(); +} + +/** + * @brief Turn on LED on matrix + * + * @param x x position of led + * @param y y position of led + * @param type type of pixel {PADDLE, BALL_RED, BALL, OFF} + */ +void Pong::toggleLed(uint8_t x, uint8_t y, uint8_t type) +{ + uint32_t color; + + switch (type) + { + case LED_TYPE_PADDLE: + color = LEDMatrix::color_24bit(0, 80, 80); + break; + case LED_TYPE_BALL_RED: + color = LEDMatrix::color_24bit(120, 0, 0); + break; + case LED_TYPE_BALL: + color = LEDMatrix::color_24bit(0, 100, 0); + break; + case LED_TYPE_OFF: + default: + color = LEDMatrix::color_24bit(0, 0, 0); + break; + } + + (*_ledmatrix).grid_add_pixel(x, y, color); +} \ No newline at end of file diff --git a/src/games/snake.cpp b/src/games/snake.cpp new file mode 100644 index 0000000..6ddc869 --- /dev/null +++ b/src/games/snake.cpp @@ -0,0 +1,315 @@ +/** + * @file snake.cpp + * @author techniccontroller (mail[at]techniccontroller.com) + * @brief Class implementation of snake game + * @version 0.1 + * @date 2022-03-05 + * + * @copyright Copyright (c) 2022 + * + * main code from https://elektro.turanis.de/html/prj099/index.html + * + */ +#include "snake.h" + +/** + * @brief Construct a new Snake:: Snake object + * + */ +Snake::Snake() +{ +} + +/** + * @brief Construct a new Snake:: Snake object + * + * @param myledmatrix pointer to LEDMatrix object, need to provide gridAddPixel(x, y, col), gridFlush() + * @param mylogger pointer to UDPLogger object, need to provide a function logString(message) + */ +Snake::Snake(LEDMatrix * matrix, UDPLogger * logger) +{ + _logger = logger; + _ledmatrix = matrix; + _gameState = GAME_STATE_END; +} + +/** + * @brief Run main loop for one cycle + * + */ +void Snake::loopCycle() +{ + switch (_gameState) + { + case GAME_STATE_INIT: + initGame(); + break; + case GAME_STATE_RUNNING: + updateGame(); + break; + case GAME_STATE_END: + break; + } +} + +/** + * @brief Trigger control: UP + * + */ +void Snake::ctrlUp() +{ + if (millis() > _lastButtonClick + DEBOUNCE_TIME && _gameState == GAME_STATE_RUNNING) + { + (*_logger).log_string("Snake: UP"); + _userDirection = DIRECTION_DOWN; // need to swap direction as field is rotated 180deg + _lastButtonClick = millis(); + } +} + +/** + * @brief Trigger control: DOWN + * + */ +void Snake::ctrlDown() +{ + if (millis() > _lastButtonClick + DEBOUNCE_TIME && _gameState == GAME_STATE_RUNNING) + { + _logger->log_string("Snake: DOWN"); + _userDirection = DIRECTION_UP; // need to swap direction as field is rotated 180deg + _lastButtonClick = millis(); + } +} + +/** + * @brief Trigger control: RIGHT + * + */ +void Snake::ctrlRight() +{ + if (millis() > _lastButtonClick + DEBOUNCE_TIME && _gameState == GAME_STATE_RUNNING) + { + _logger->log_string("Snake: RIGHT"); + _userDirection = DIRECTION_LEFT; // need to swap direction as field is rotated 180deg + _lastButtonClick = millis(); + } +} + +/** + * @brief Trigger control: LEFT + * + */ +void Snake::ctrlLeft() +{ + if (millis() > _lastButtonClick + DEBOUNCE_TIME && _gameState == GAME_STATE_RUNNING) + { + _logger->log_string("Snake: LEFT"); + _userDirection = DIRECTION_RIGHT; // need to swap direction as field is rotated 180deg + _lastButtonClick = millis(); + } +} + +/** + * @brief Clear the led matrix (turn all leds off) + * + */ +void Snake::resetLEDs() +{ + _ledmatrix->flush(); +} + +/** + * @brief Initialize a new game + * + */ +void Snake::initGame() +{ + _logger->log_string("Snake: init"); + resetLEDs(); + _head.x = 0; + _head.y = 0; + _food.x = -1; + _food.y = -1; + _wormLength = MIN_TAIL_LENGTH; + _userDirection = DIRECTION_LEFT; + _lastButtonClick = millis(); + + for (int i = 0; i < MAX_TAIL_LENGTH; i++) + { + _tail[i].x = -1; + _tail[i].y = -1; + } + updateFood(); + _gameState = GAME_STATE_RUNNING; +} + +/** + * @brief Update game representation + * + */ +void Snake::updateGame() +{ + if ((millis() - _lastDrawUpdate) > GAME_DELAY) + { + _logger->log_string("Snake: update game"); + toggleLed(_tail[_wormLength - 1].x, _tail[_wormLength - 1].y, LED_TYPE_OFF); + switch (_userDirection) + { + case DIRECTION_RIGHT: + if (_head.x > 0) + { + _head.x--; + } + break; + case DIRECTION_LEFT: + if (_head.x < X_MAX - 1) + { + _head.x++; + } + break; + case DIRECTION_DOWN: + if (_head.y > 0) + { + _head.y--; + } + break; + case DIRECTION_UP: + if (_head.y < Y_MAX - 1) + { + _head.y++; + } + break; + } + + if (isCollision() == true) + { + endGame(); + return; + } + + updateTail(); + + if (_head.x == _food.x && _head.y == _food.y) + { + if (_wormLength < MAX_TAIL_LENGTH) + { + _wormLength++; + } + updateFood(); + } + + _lastDrawUpdate = millis(); + } +} + +/** + * @brief Game over, draw _head red + * + */ +void Snake::endGame() +{ + _gameState = GAME_STATE_END; + toggleLed(_head.x, _head.y, LED_TYPE_BLOOD); +} + +/** + * @brief Update _tail led positions + * + */ +void Snake::updateTail() +{ + for (unsigned int i = _wormLength - 1; i > 0; i--) + { + _tail[i].x = _tail[i - 1].x; + _tail[i].y = _tail[i - 1].y; + } + _tail[0].x = _head.x; + _tail[0].y = _head.y; + + for (unsigned int i = 0; i < _wormLength; i++) + { + if (_tail[i].x > -1) + { + toggleLed(_tail[i].x, _tail[i].y, LED_TYPE_SNAKE); + } + } +} + +/** + * @brief Update _food position (generate new one if found) + * + */ +void Snake::updateFood() +{ + bool found = true; + do + { + found = true; + _food.x = random(0, X_MAX); + _food.y = random(0, Y_MAX); + for (unsigned int i = 0; i < _wormLength; i++) + { + if (_tail[i].x == _food.x && _tail[i].y == _food.y) + { + found = false; + } + } + } while (found == false); + toggleLed(_food.x, _food.y, LED_TYPE_FOOD); +} + +/** + * @brief Check for collisison between snake and border or itself + * + * @return true + * @return false + */ +bool Snake::isCollision() +{ + if (_head.x < 0 || _head.x >= X_MAX) + { + return true; + } + if (_head.y < 0 || _head.y >= Y_MAX) + { + return true; + } + for (unsigned int i = 1; i < _wormLength; i++) + { + if (_tail[i].x == _head.x && _tail[i].y == _head.y) + { + return true; + } + } + return false; +} + +/** + * @brief Turn on LED on matrix + * + * @param x x position of led + * @param y y position of led + * @param type type of pixel {SNAKE, OFF, FOOD, BLOOD} + */ +void Snake::toggleLed(uint8_t x, uint8_t y, uint8_t type) +{ + uint32_t color; + + switch (type) + { + case LED_TYPE_SNAKE: + color = LEDMatrix::color_24bit(0, 100, 100); + break; + case LED_TYPE_OFF: + color = LEDMatrix::color_24bit(0, 0, 0); + break; + case LED_TYPE_FOOD: + color = LEDMatrix::color_24bit(0, 150, 0); + break; + case LED_TYPE_BLOOD: + default: + color = LEDMatrix::color_24bit(150, 0, 0); + break; + } + + _ledmatrix->grid_add_pixel(x, y, color); +} \ No newline at end of file diff --git a/src/games/tetris.cpp b/src/games/tetris.cpp new file mode 100644 index 0000000..c59b561 --- /dev/null +++ b/src/games/tetris.cpp @@ -0,0 +1,680 @@ +/** + * @file tetris.cpp + * @author techniccontroller (mail[at]techniccontroller.com) + * @brief Class implementation for tetris game + * @version 0.1 + * @date 2022-03-05 + * + * @copyright Copyright (c) 2022 + * + * main tetris code originally written by Klaas De Craemer, Ing. David Hrbaty + * + */ +#include "tetris.h" + +Tetris::Tetris() +{ +} + +/** + * @brief Construct a new Tetris:: Tetris object + * + * @param myledmatrix pointer to LEDMatrix object, need to provide gridAddPixel(x, y, col), draw_on_matrix(), gridFlush() and printNumber(x,y,n,col) + * @param mylogger pointer to UDPLogger object, need to provide a function log_string(message) + */ +Tetris::Tetris(LEDMatrix *myledmatrix, UDPLogger *mylogger) +{ + _logger = mylogger; + _ledmatrix = myledmatrix; + _gameStatet = GAME_STATE_READY; +} + +/** + * @brief Run main loop for one cycle + * + */ +void Tetris::loopCycle() +{ + switch (_gameStatet) + { + case GAME_STATE_READY: + + break; + case GAME_STATE_INIT: + tetrisInit(); + + break; + case GAME_STATE_RUNNING: + // If brick is still "on the loose", then move it down by one + if (_activeBrick.enabled) + { + // move faster down when allow drop + if (_allowdrop) + { + if (millis() > _droptime + 50) + { + _droptime = millis(); + shiftActiveBrick(DIR_DOWN); + printField(); + } + } + + // move down with regular speed + if ((millis() - _prevUpdateTime) > (_brickSpeed * _speedtetris / 100)) + { + _prevUpdateTime = millis(); + shiftActiveBrick(DIR_DOWN); + printField(); + } + } + else + { + _allowdrop = false; + // Active brick has "crashed", check for full lines + // and create new brick at top of field + checkFullLines(); + newActiveBrick(); + _prevUpdateTime = millis(); // Reset update time to avoid brick dropping two spaces + } + break; + case GAME_STATE_PAUSED: + + break; + case GAME_STATE_END: + // at game end show all bricks on field in red color for 1.5 seconds, then show score + if (_tetrisGameOver == true) + { + _tetrisGameOver = false; + (*_logger).log_string("Tetris: end"); + everythingRed(); + _tetrisshowscore = millis(); + } + + if (millis() > (_tetrisshowscore + RED_END_TIME)) + { + resetLEDs(); + _score = _nbRowsTotal; + showscore(); + _gameStatet = GAME_STATE_READY; + } + break; + } +} + +/** + * @brief Trigger control: START (& restart) + * + */ +void Tetris::ctrlStart() +{ + if (millis() > _lastButtonClick + DEBOUNCE_TIME) + { + _lastButtonClick = millis(); + _gameStatet = GAME_STATE_INIT; + } +} + +/** + * @brief Trigger control: PAUSE/PLAY + * + */ +void Tetris::ctrlPlayPause() +{ + if (millis() > _lastButtonClick + DEBOUNCE_TIME) + { + _lastButtonClick = millis(); + if (_gameStatet == GAME_STATE_PAUSED) + { + (*_logger).log_string("Tetris: continue"); + + _gameStatet = GAME_STATE_RUNNING; + } + else if (_gameStatet == GAME_STATE_RUNNING) + { + (*_logger).log_string("Tetris: pause"); + + _gameStatet = GAME_STATE_PAUSED; + } + } +} + +/** + * @brief Trigger control: RIGHT + * + */ +void Tetris::ctrlRight() +{ + if (millis() > _lastButtonClick + DEBOUNCE_TIME && _gameStatet == GAME_STATE_RUNNING) + { + _lastButtonClick = millis(); + shiftActiveBrick(DIR_RIGHT); + printField(); + } +} + +/** + * @brief Trigger control: LEFT + * + */ +void Tetris::ctrlLeft() +{ + if (millis() > _lastButtonClick + DEBOUNCE_TIME && _gameStatet == GAME_STATE_RUNNING) + { + _lastButtonClick = millis(); + shiftActiveBrick(DIR_LEFT); + printField(); + } +} + +/** + * @brief Trigger control: UP (rotate) + * + */ +void Tetris::ctrlUp() +{ + if (millis() > _lastButtonClick + DEBOUNCE_TIME && _gameStatet == GAME_STATE_RUNNING) + { + _lastButtonClick = millis(); + rotateActiveBrick(); + printField(); + } +} + +/** + * @brief Trigger control: DOWN (drop) + * + */ +void Tetris::ctrlDown() +{ + // longer debounce time, to prevent immediate drop + if (millis() > _lastButtonClickr + DEBOUNCE_TIME * 5 && _gameStatet == GAME_STATE_RUNNING) + { + _allowdrop = true; + _lastButtonClickr = millis(); + } +} + +/** + * @brief Set game speed + * + * @param i new speed value + */ +void Tetris::setSpeed(int32_t i) +{ + _logger->log_string("setSpeed: " + String(i)); + _speedtetris = -10 * i + 150; +} + +/** + * @brief Clear the led matrix (turn all leds off) + * + */ +void Tetris::resetLEDs() +{ + _ledmatrix->flush(); + _ledmatrix->draw_on_matrix_instant(); +} + +/** + * @brief Initialize the tetris game + * + */ +void Tetris::tetrisInit() +{ + (*_logger).log_string("Tetris: init"); + + clearField(); + _brickSpeed = INIT_SPEED; + _nbRowsThisLevel = 0; + _nbRowsTotal = 0; + _tetrisGameOver = false; + + newActiveBrick(); + _prevUpdateTime = millis(); + + _gameStatet = GAME_STATE_RUNNING; +} + +/** + * @brief Draw current field representation to led matrix + * + */ +void Tetris::printField() +{ + int x, y; + for (x = 0; x < MATRIX_WIDTH; x++) + { + for (y = 0; y < MATRIX_HEIGHT; y++) + { + uint8_t activeBrickPix = 0; + if (_activeBrick.enabled) + { // Only draw brick if it is enabled + // Now check if brick is "in view" + if ((x >= _activeBrick.xpos) && (x < (_activeBrick.xpos + (_activeBrick.siz))) && (y >= _activeBrick.ypos) && (y < (_activeBrick.ypos + (_activeBrick.siz)))) + { + activeBrickPix = (_activeBrick.pix)[x - _activeBrick.xpos][y - _activeBrick.ypos]; + } + } + if (_field.pix[x][y] == 1) + { + _ledmatrix->grid_add_pixel(x, y, _field.color[x][y]); + } + else if (activeBrickPix == 1) + { + _ledmatrix->grid_add_pixel(x, y, _activeBrick.col); + } + else + { + _ledmatrix->grid_add_pixel(x, y, 0x000000); + } + } + } + _ledmatrix->draw_on_matrix_instant(); +} + +/* *** Game functions *** */ +/** + * @brief Spawn new (random) brick + * + */ +void Tetris::newActiveBrick() +{ + uint8_t selectedBrick = 0; + static uint8_t lastselectedBrick = 0; + + // choose random next brick, but not the same as before + do + { + selectedBrick = random(7); + } while (lastselectedBrick == selectedBrick); + + // Save selected brick for next round + lastselectedBrick = selectedBrick; + + // every brick has its color, select corresponding color + uint32_t selectedCol = _brickLib[selectedBrick].col; + // Set properties of brick + _activeBrick.siz = _brickLib[selectedBrick].siz; + _activeBrick.yOffset = _brickLib[selectedBrick].yOffset; + _activeBrick.xpos = MATRIX_WIDTH / 2 - _activeBrick.siz / 2; + _activeBrick.ypos = BRICKOFFSET - _activeBrick.yOffset; + _activeBrick.enabled = true; + + // Set color of brick + _activeBrick.col = selectedCol; + // _activeBrick.color = _colorLib[1]; + + // Copy pix array of selected Brick + uint8_t x, y; + for (y = 0; y < MAX_BRICK_SIZE; y++) + { + for (x = 0; x < MAX_BRICK_SIZE; x++) + { + _activeBrick.pix[x][y] = (_brickLib[selectedBrick]).pix[x][y]; + } + } + + // Check collision, if already, then game is over + if (checkFieldCollision(&_activeBrick)) + { + _tetrisGameOver = true; + _gameStatet = GAME_STATE_END; + } +} + +/** + * @brief Check collision between bricks in the field and the specified brick + * + * @param brick brick to be checked for collision + * @return boolean true if collision occured + */ +boolean Tetris::checkFieldCollision(struct Brick *brick) +{ + uint8_t bx, by; + uint8_t fx, fy; + for (by = 0; by < MAX_BRICK_SIZE; by++) + { + for (bx = 0; bx < MAX_BRICK_SIZE; bx++) + { + fx = brick->xpos + bx; + fy = brick->ypos + by; + if ((brick->pix[bx][by] == 1) && (_field.pix[fx][fy] == 1)) + { + return true; + } + } + } + return false; +} + +/** + * @brief Check collision between specified brick and all sides of the playing field + * + * @param brick brick to be checked for collision + * @return boolean true if collision occured + */ +boolean Tetris::checkSidesCollision(struct Brick *brick) +{ + // Check vertical collision with sides of field + uint8_t bx, by; + uint8_t fx; //, fy; /* Patch */ + for (by = 0; by < MAX_BRICK_SIZE; by++) + { + for (bx = 0; bx < MAX_BRICK_SIZE; bx++) + { + if (brick->pix[bx][by] == 1) + { + fx = brick->xpos + bx; // Determine actual position in the field of the current pix of the brick + // fy = brick->ypos + by; /* Patch */ + if (fx < 0 || fx >= MATRIX_WIDTH) + { + return true; + } + } + } + } + return false; +} + +/** + * @brief Rotate current active brick + * + */ +void Tetris::rotateActiveBrick() +{ + // Copy active brick pix array to temporary pix array + uint8_t x, y; + Brick tmpBrick; + for (y = 0; y < MAX_BRICK_SIZE; y++) + { + for (x = 0; x < MAX_BRICK_SIZE; x++) + { + tmpBrick.pix[x][y] = _activeBrick.pix[x][y]; + } + } + tmpBrick.xpos = _activeBrick.xpos; + tmpBrick.ypos = _activeBrick.ypos; + tmpBrick.siz = _activeBrick.siz; + + // Depending on size of the active brick, we will rotate differently + if (_activeBrick.siz == 3) + { + // Perform rotation around center pix + tmpBrick.pix[0][0] = _activeBrick.pix[0][2]; + tmpBrick.pix[0][1] = _activeBrick.pix[1][2]; + tmpBrick.pix[0][2] = _activeBrick.pix[2][2]; + tmpBrick.pix[1][0] = _activeBrick.pix[0][1]; + tmpBrick.pix[1][1] = _activeBrick.pix[1][1]; + tmpBrick.pix[1][2] = _activeBrick.pix[2][1]; + tmpBrick.pix[2][0] = _activeBrick.pix[0][0]; + tmpBrick.pix[2][1] = _activeBrick.pix[1][0]; + tmpBrick.pix[2][2] = _activeBrick.pix[2][0]; + // Keep other parts of temporary block clear + tmpBrick.pix[0][3] = 0; + tmpBrick.pix[1][3] = 0; + tmpBrick.pix[2][3] = 0; + tmpBrick.pix[3][3] = 0; + tmpBrick.pix[3][2] = 0; + tmpBrick.pix[3][1] = 0; + tmpBrick.pix[3][0] = 0; + } + else if (_activeBrick.siz == 4) + { + // Perform rotation around center "cross" + tmpBrick.pix[0][0] = _activeBrick.pix[0][3]; + tmpBrick.pix[0][1] = _activeBrick.pix[1][3]; + tmpBrick.pix[0][2] = _activeBrick.pix[2][3]; + tmpBrick.pix[0][3] = _activeBrick.pix[3][3]; + tmpBrick.pix[1][0] = _activeBrick.pix[0][2]; + tmpBrick.pix[1][1] = _activeBrick.pix[1][2]; + tmpBrick.pix[1][2] = _activeBrick.pix[2][2]; + tmpBrick.pix[1][3] = _activeBrick.pix[3][2]; + tmpBrick.pix[2][0] = _activeBrick.pix[0][1]; + tmpBrick.pix[2][1] = _activeBrick.pix[1][1]; + tmpBrick.pix[2][2] = _activeBrick.pix[2][1]; + tmpBrick.pix[2][3] = _activeBrick.pix[3][1]; + tmpBrick.pix[3][0] = _activeBrick.pix[0][0]; + tmpBrick.pix[3][1] = _activeBrick.pix[1][0]; + tmpBrick.pix[3][2] = _activeBrick.pix[2][0]; + tmpBrick.pix[3][3] = _activeBrick.pix[3][0]; + } + else + { + _logger->log_string("Tetris: Brick size error"); + } + + // Now validate by checking collision. + // Collision possibilities: + // - Brick now sticks outside field + // - Brick now sticks inside fixed bricks of field + // In case of collision, we just discard the rotated temporary brick + if ((!checkSidesCollision(&tmpBrick)) && (!checkFieldCollision(&tmpBrick))) + { + // Copy temporary brick pix array to active pix array + for (y = 0; y < MAX_BRICK_SIZE; y++) + { + for (x = 0; x < MAX_BRICK_SIZE; x++) + { + _activeBrick.pix[x][y] = tmpBrick.pix[x][y]; + } + } + } +} + +/** + * @brief Shift brick left/right/down by one if possible + * + * @param dir direction to be shifted + */ +void Tetris::shiftActiveBrick(int dir) +{ + // Change position of active brick (no copy to temporary needed) + if (dir == DIR_LEFT) + { + _activeBrick.xpos--; + } + else if (dir == DIR_RIGHT) + { + _activeBrick.xpos++; + } + else if (dir == DIR_DOWN) + { + _activeBrick.ypos++; + } + + // Check position of active brick + // Two possibilities when collision is detected: + // - Direction was LEFT/RIGHT, just revert position back + // - Direction was DOWN, revert position and fix block to field on collision + // When no collision, keep _activeBrick coordinates + if ((checkSidesCollision(&_activeBrick)) || (checkFieldCollision(&_activeBrick))) + { + if (dir == DIR_LEFT) + { + _activeBrick.xpos++; + } + else if (dir == DIR_RIGHT) + { + _activeBrick.xpos--; + } + else if (dir == DIR_DOWN) + { + _activeBrick.ypos--; // Go back up one + addActiveBrickToField(); + _activeBrick.enabled = false; // Disable brick, it is no longer moving + } + } +} + +/** + * @brief Copy active pixels to field, including color + * + */ +void Tetris::addActiveBrickToField() +{ + uint8_t bx, by; + uint8_t fx, fy; + for (by = 0; by < MAX_BRICK_SIZE; by++) + { + for (bx = 0; bx < MAX_BRICK_SIZE; bx++) + { + fx = _activeBrick.xpos + bx; + fy = _activeBrick.ypos + by; + + if (fx >= 0 && fy >= 0 && fx < MATRIX_WIDTH && fy < MATRIX_HEIGHT && _activeBrick.pix[bx][by]) + { // Check if inside playing field + // _field.pix[fx][fy] = _field.pix[fx][fy] || _activeBrick.pix[bx][by]; + _field.pix[fx][fy] = _activeBrick.pix[bx][by]; + _field.color[fx][fy] = _activeBrick.col; + } + } + } +} + +/** + * @brief Move all pix from the field above startRow down by one. startRow is overwritten + * + * @param startRow + */ +void Tetris::moveFieldDownOne(uint8_t startRow) +{ + if (startRow == 0) + { // Topmost row has nothing on top to move... + return; + } + uint8_t x, y; + for (y = startRow - 1; y > 0; y--) + { + for (x = 0; x < MATRIX_WIDTH; x++) + { + _field.pix[x][y + 1] = _field.pix[x][y]; + _field.color[x][y + 1] = _field.color[x][y]; + } + } +} + +/** + * @brief Check if a line is complete + * + */ +void Tetris::checkFullLines() +{ + int x, y; + int minY = 0; + for (y = (MATRIX_HEIGHT - 1); y >= minY; y--) + { + uint8_t rowSum = 0; + for (x = 0; x < MATRIX_WIDTH; x++) + { + rowSum = rowSum + (_field.pix[x][y]); + } + if (rowSum >= MATRIX_WIDTH) + { + // Found full row, animate its removal + _activeBrick.enabled = false; + + for (x = 0; x < MATRIX_WIDTH; x++) + { + _field.pix[x][y] = 0; + printField(); + delay(100); + } + // Move all upper rows down by one + moveFieldDownOne(y); + y++; + minY++; + printField(); + delay(100); + + _nbRowsThisLevel++; + _nbRowsTotal++; + if (_nbRowsThisLevel >= LEVELUP) + { + _nbRowsThisLevel = 0; + _brickSpeed = _brickSpeed - SPEED_STEP; + if (_brickSpeed < 200) + { + _brickSpeed = 200; + } + } + } + } +} + +/** + * @brief Clear field + * + */ +void Tetris::clearField() +{ + uint8_t x, y; + for (y = 0; y < MATRIX_HEIGHT; y++) + { + for (x = 0; x < MATRIX_WIDTH; x++) + { + _field.pix[x][y] = 0; + _field.color[x][y] = 0; + } + } + for (x = 0; x < MATRIX_WIDTH; x++) + { // This last row is invisible to the player and only used for the collision detection routine + _field.pix[x][MATRIX_HEIGHT] = 1; + } +} + +/** + * @brief Color all bricks on the field red + * + */ +void Tetris::everythingRed() +{ + int x, y; + for (x = 0; x < MATRIX_WIDTH; x++) + { + for (y = 0; y < MATRIX_HEIGHT; y++) + { + uint8_t activeBrickPix = 0; + if (_activeBrick.enabled) + { // Only draw brick if it is enabled + // Now check if brick is "in view" + if ((x >= _activeBrick.xpos) && (x < (_activeBrick.xpos + (_activeBrick.siz))) && (y >= _activeBrick.ypos) && (y < (_activeBrick.ypos + (_activeBrick.siz)))) + { + activeBrickPix = (_activeBrick.pix)[x - _activeBrick.xpos][y - _activeBrick.ypos]; + } + } + if (_field.pix[x][y] == 1) + { + _ledmatrix->grid_add_pixel(x, y, RED); + } + else if (activeBrickPix == 1) + { + _ledmatrix->grid_add_pixel(x, y, RED); + } + else + { + _ledmatrix->grid_add_pixel(x, y, 0x000000); + } + } + } + _ledmatrix->draw_on_matrix_instant(); +} + +/** + * @brief Draw score to led matrix + * + */ +void Tetris::showscore() +{ + uint32_t color = LEDMatrix::color_24bit(255, 170, 0); + _ledmatrix->flush(); + if (_score > 9) + { + _ledmatrix->print_number(2, 3, _score / 10, color); + _ledmatrix->print_number(6, 3, _score % 10, color); + } + else + { + _ledmatrix->print_number(4, 3, _score, color); + } + _ledmatrix->draw_on_matrix_instant(); +} \ No newline at end of file diff --git a/src/matrix/animationfunctions.cpp b/src/matrix/animationfunctions.cpp new file mode 100644 index 0000000..b3fe13a --- /dev/null +++ b/src/matrix/animationfunctions.cpp @@ -0,0 +1,439 @@ +#include +#include "animationfunctions.h" +#include "wordclock_constants.h" +#include "udp_logger.h" +#include "ledmatrix.h" + +extern UDPLogger logger; +extern LEDMatrix led_matrix; + +const int8_t dx[] = {1, -1, 0, 0}; +const int8_t dy[] = {0, 0, -1, 1}; + +/** + * @brief Function to draw a spiral step (from center) + * + * @param init marks if call is the initial step of the spiral + * @param empty marks if the spiral should 'draw' empty leds + * @param size the size of the spiral in leds + * @return int - 1 if end is reached, else 0 + */ +int draw_spiral(bool init, bool empty, uint8_t size) +{ + static Direction dir; // current direction + static int x; + static int y; + static int counter; + static int count_step; + static int count_edge; + static int count_corner; + static bool expand; + static int random_num; + + if (init) + { + logger.log_string("Init Spiral with empty=" + String(empty)); + dir = DOWN; // current direction + x = MATRIX_WIDTH / 2; + y = MATRIX_WIDTH / 2; + if (!empty) + { + led_matrix.flush(); + } + counter = 0; + count_step = 0; + count_edge = 1; + count_corner = 0; + expand = true; + random_num = random(UINT8_MAX); + } + + if (count_step == size * size) + { + // End reached return 1 + return 1; + } + else + { + // calc color from colorwheel + uint32_t color = LEDMatrix::wheel((random_num + count_step * 6) % UINT8_MAX); + // if draw mode is empty, set color to zero + if (empty) + { + color = 0; + } + led_matrix.grid_add_pixel(x, y, color); + if (count_corner == 2 && expand) + { + count_edge += 1; + expand = false; + } + if (counter >= count_edge) + { + dir = next_direction(dir, LEFT); + counter = 0; + count_corner++; + } + if (count_corner >= 4) + { + count_corner = 0; + count_edge += 1; + expand = true; + } + + x += dx[dir]; + y += dy[dir]; + // logger.log_string("x: " + String(x) + ", y: " + String(y) + "c: " + String(color) + "\n"); + counter++; + count_step++; + } + return 0; +} + +/** + * @brief Run random snake animation + * + * @param init marks if call is the initial step of the animation + * @param len length of the snake + * @param color color of the snake + * @param numSteps number of animation steps + * @return int - 1 when animation is finished, else 0 + */ +int random_snake(bool init, const uint8_t len, const uint32_t color, int numSteps) +{ + static Direction dir; + static int snake[2][10]; + static int random_y; + static int random_x; + static int e; + static int countStep; + if (init) + { + dir = DOWN; // current direction + for (int i = 0; i < len; i++) + { + snake[0][i] = 3; + snake[1][i] = i; + } + + random_y = random(1, 8); // Random variable for y-direction + random_x = random(1, 4); // Random variable for x-direction + e = LEFT; // next turn + countStep = 0; + } + if (countStep == numSteps) + { + // End reached return 1 + return 1; + } + else + { + // move one step forward + for (int i = 0; i < len; i++) + { + if (i < len - 1) + { + snake[0][i] = snake[0][i + 1]; + snake[1][i] = snake[1][i + 1]; + } + else + { + snake[0][i] = snake[0][i] + dx[dir]; + snake[1][i] = snake[1][i] + dy[dir]; + } + } + // collision with wall? + if ((dir == DOWN && snake[1][len - 1] >= MATRIX_HEIGHT - 1) || + (dir == UP && snake[1][len - 1] <= 0) || + (dir == RIGHT && snake[0][len - 1] >= MATRIX_WIDTH - 1) || + (dir == LEFT && snake[0][len - 1] <= 0)) + { + dir = next_direction(dir, e); + } + // Random branching at the side edges + else if ((dir == UP && snake[1][len - 1] == random_y && snake[0][len - 1] >= MATRIX_WIDTH - 1) || (dir == DOWN && snake[1][len - 1] == random_y && snake[0][len - 1] <= 0)) + { + dir = next_direction(dir, LEFT); + e = (e + 2) % 2 + 1; + } + else if ((dir == DOWN && snake[1][len - 1] == random_y && snake[0][len - 1] >= MATRIX_WIDTH - 1) || (dir == UP && snake[1][len - 1] == random_y && snake[0][len - 1] <= 0)) + { + dir = next_direction(dir, RIGHT); + e = (e + 2) % 2 + 1; + } + else if ((dir == LEFT && snake[0][len - 1] == random_x && snake[1][len - 1] <= 0) || (dir == RIGHT && snake[0][len - 1] == random_x && snake[1][len - 1] >= MATRIX_HEIGHT - 1)) + { + dir = next_direction(dir, LEFT); + e = (e + 2) % 2 + 1; + } + else if ((dir == RIGHT && snake[0][len - 1] == random_x && snake[1][len - 1] <= 0) || (dir == LEFT && snake[0][len - 1] == random_x && snake[1][len - 1] >= MATRIX_HEIGHT - 1)) + { + dir = next_direction(dir, RIGHT); + e = (e + 2) % 2 + 1; + } + + for (int i = 0; i < len; i++) + { + // draw the snake + led_matrix.grid_add_pixel(snake[0][i], snake[1][i], color); + } + + // calc new random variables after every 20 steps + if (countStep % 20 == 0) + { + random_y = random(1, 8); + random_x = random(1, 4); + } + countStep++; + } + return 0; +} + +/** + * @brief Calc the next direction for led movement (snake and spiral) + * + * @param dir direction of the current led movement + * @param d action to be executed + * @return direction - next direction + */ +Direction next_direction(Direction dir, int d) +{ + // d = 0 -> continue straight on + // d = 1 -> turn LEFT + // d = 2 -> turn RIGHT + Direction selection[3]; + switch (dir) + { + case RIGHT: + { + selection[0] = RIGHT; + selection[1] = UP; + selection[2] = DOWN; + break; + } + case LEFT: + { + selection[0] = LEFT; + selection[1] = DOWN; + selection[2] = UP; + break; + } + case UP: + { + selection[0] = UP; + selection[1] = LEFT; + selection[2] = RIGHT; + break; + } + case DOWN: + { + selection[0] = DOWN; + selection[1] = RIGHT; + selection[2] = LEFT; + break; + } + } + Direction next = selection[d]; + return next; +} + +/** + * @brief Show the time as digits on the wordclock + * + * @param hours hours of time to display + * @param minutes minutes of time to display + * @param color color to display (24bit) + */ +void show_digital_clock(uint8_t hours, uint8_t minutes, uint32_t color) +{ + led_matrix.flush(); + uint8_t fstDigitH = hours / 10; + uint8_t sndDigitH = hours % 10; + uint8_t fstDigitM = minutes / 10; + uint8_t sndDigitM = minutes % 10; + led_matrix.print_number(2, 0, fstDigitH, color); + led_matrix.print_number(6, 0, sndDigitH, color); + led_matrix.print_number(2, 6, fstDigitM, color); + led_matrix.print_number(6, 6, sndDigitM, color); +} + +/** + * @brief Run random tetris animation + * + * @param init marks if call is the initial step of the animation + * @return int - 1 when animation is finished, else 0 + */ +int random_tetris(bool init) +{ + // total number of blocks which can be displayed + const static uint8_t numBlocks = 30; + // all different block shapes + const static bool blockshapes[9][3][3] = {{{0, 0, 0}, + {0, 0, 0}, + {0, 0, 0}}, + {{1, 0, 0}, + {1, 0, 0}, + {1, 0, 0}}, + {{0, 0, 0}, + {1, 0, 0}, + {1, 0, 0}}, + {{0, 0, 0}, + {1, 1, 0}, + {1, 0, 0}}, + {{0, 0, 0}, + {0, 0, 0}, + {1, 1, 0}}, + {{0, 0, 0}, + {1, 1, 0}, + {1, 1, 0}}, + {{0, 0, 0}, + {0, 0, 0}, + {1, 1, 1}}, + {{0, 0, 0}, + {1, 1, 1}, + {1, 0, 0}}, + {{0, 0, 0}, + {0, 0, 1}, + {1, 1, 1}}}; + // local game screen buffer + static uint8_t screen[MATRIX_HEIGHT + 3][MATRIX_WIDTH]; + // current number of blocks on the screen + static int counterID; + // indicate if the game was lost + static bool gameover = false; + + if (init || gameover) + { + logger.log_string("Init Tetris: init=" + String(init) + ", gameover=" + String(gameover)); + // clear local game screen + for (int h = 0; h < MATRIX_HEIGHT + 3; h++) + { + for (int w = 0; w < MATRIX_WIDTH; w++) + { + screen[h][w] = 0; + } + } + counterID = 0; + gameover = false; + } + else + { + led_matrix.flush(); + + // list of all blocks in game, indicating which are moving + // set every block on the screen as a potentially mover + bool to_move[numBlocks + 1]; + for (int i = 0; i < numBlocks; i++) + to_move[i + 1] = i < counterID; + + // identify tiles which can move DOWN (no collision below) + for (int c = 0; c < MATRIX_WIDTH; c++) + { // columns + for (int r = 0; r < MATRIX_HEIGHT + 3; r++) + { // rows + // only check pixels which are occupied + if (screen[r][c] != 0) + { + // every tile which has a pixel in last row -> no mover + if (r == MATRIX_HEIGHT + 2) + { + to_move[screen[r][c]] = false; + } + // or every pixel + else if (screen[r + 1][c] != 0 && screen[r + 1][c] != screen[r][c]) + { + to_move[screen[r][c]] = false; + } + } + } + } + + // indicate if there is no moving block + // assume first there are no more moving block + bool no_more_mover = true; + // loop over existing block and ask if they can move + for (int i = 0; i < counterID; i++) + { + if (to_move[i + 1]) + { + no_more_mover = false; + } + } + + if (no_more_mover) + { + // no more moving blocks -> check if game over or spawn new block + logger.log_string("Tetris: No more Mover"); + gameover = false; + // check if game was lost -> one pixel active in 4rd row (top row on the led grid) + for (int s = 0; s < MATRIX_WIDTH; s++) + { + if (screen[3][s] != 0) + gameover = true; + } + if (gameover || counterID >= (numBlocks - 1)) + { + logger.log_string("Tetris: Gameover"); + return 1; + } + + // Create new block + // increment counter + counterID++; + // select random shape for new block + uint8_t randShape = random(1, 9); + // select random position (column) for spawn of new block + uint8_t randx = random(0, MATRIX_WIDTH - 3); + // copy shape to screen (c1 - column of block, c2 - column of screen) + // write the id of block on the screen + for (int c1 = 0, c2 = randx; c1 < 3; c1++, c2++) + { + for (int r = 0; r < 3; r++) + { + if (blockshapes[randShape][r][c1]) + screen[r][c2] = counterID; + } + } + } + else + { + // uint8_t tempscreen[MATRIX_HEIGHT + 3][MATRIX_WIDTH] = {0}; + uint8_t moveX = MATRIX_WIDTH - 1; + uint8_t moveY = MATRIX_HEIGHT + 2; + // moving blocks exists -> move them one pixel DOWN + // loop over pixels and move every pixel DOWN, which belongs to a moving block + for (int c = MATRIX_WIDTH - 1; c >= 0; c--) + { + for (int r = MATRIX_HEIGHT + 1; r >= 0; r--) + { + if ((screen[r][c] != 0) && to_move[screen[r][c]]) + { + // tempscreen[r + 1][c] = screen[r][c]; + screen[r + 1][c] = screen[r][c]; + screen[r][c] = 0; + // save top LEFT corner of block + if (moveX > c) + moveX = c; + if (moveY > r) + moveY = r; + } + } + } + } + + // draw/copy screen values to led grid (r - row, c - column) + for (int c = 0; c < MATRIX_WIDTH; c++) + { + for (int r = 0; r < MATRIX_HEIGHT; r++) + { + if (screen[r + 3][c] != 0) + { + // screen is 3 pixels higher than led grid, so drop the upper three lines + led_matrix.grid_add_pixel(c, r, colors_24bit[(screen[r + 3][c] % NUM_COLORS)]); + } + } + } + return 0; + } + return 0; +} diff --git a/src/matrix/ledmatrix.cpp b/src/matrix/ledmatrix.cpp new file mode 100644 index 0000000..b63c4eb --- /dev/null +++ b/src/matrix/ledmatrix.cpp @@ -0,0 +1,343 @@ +#include "ledmatrix.h" +#include "own_font.h" +#include "wordclock_constants.h" + +#define MAX_LED_CURRENT_MA 20 // 20mA for full brightness per LED + +// seven predefined colors24bit (green, red, yellow, purple, orange, lightgreen, blue) +const uint32_t colors_24bit[NUM_COLORS] = { + LEDMatrix::color_24bit(0, 255, 0), + LEDMatrix::color_24bit(255, 0, 0), + LEDMatrix::color_24bit(200, 200, 0), + LEDMatrix::color_24bit(255, 0, 200), + LEDMatrix::color_24bit(255, 128, 0), + LEDMatrix::color_24bit(0, 128, 0), + LEDMatrix::color_24bit(0, 0, 255)}; + +/** + * @brief Construct a new LEDMatrix::LEDMatrix object + * + * @param mymatrix pointer to Adafruit_NeoMatrix object + * @param mybrightness the initial brightness of the leds + * @param mylogger pointer to the UDPLogger object + */ +LEDMatrix::LEDMatrix(Adafruit_NeoMatrix * matrix, uint8_t brightness, UDPLogger * logger) +{ + _neomatrix = matrix; + _brightness = brightness; + _logger = logger; + _current_limit = DEFAULT_CURRENT_LIMIT; +} + +/** + * @brief Convert RGB value to 24bit color value + * + * @param r red value (0-255) + * @param g green value (0-255) + * @param b blue value (0-255) + * @return uint32_t 24bit color value + */ +uint32_t LEDMatrix::color_24bit(uint8_t r, uint8_t g, uint8_t b) +{ + return ((uint32_t)r << 16) | ((uint32_t)g << 8) | b; +} + +/** + * @brief Convert 24bit color to 16bit color + * + * @param color24bit 24bit color value + * @return uint16_t 16bit color value + */ +uint16_t LEDMatrix::color_24_to_16bit(uint32_t color_24bit) +{ + uint8_t r = color_24bit >> 16 & 0xff; + uint8_t g = color_24bit >> 8 & 0xff; + uint8_t b = color_24bit & 0xff; + return ((uint16_t)(r & 0xF8) << 8) | + ((uint16_t)(g & 0xFC) << 3) | + (b >> 3); +} + +/** + * @brief Input a value 0 to 255 to get a color value. The colors are a transition r - g - b - back to r. + * + * @param WheelPos Value between 0 and 255 + * @return uint32_t return 24bit color of colorwheel + */ +uint32_t LEDMatrix::wheel(uint8_t WheelPos) +{ + WheelPos = UINT8_MAX - WheelPos; + if (WheelPos < 85) + { + return color_24bit(UINT8_MAX - WheelPos * 3, 0, WheelPos * 3); + } + if (WheelPos < 170) + { + WheelPos -= 85; + return color_24bit(0, WheelPos * 3, UINT8_MAX - WheelPos * 3); + } + WheelPos -= 170; + return color_24bit(WheelPos * 3, UINT8_MAX - WheelPos * 3, 0); +} + +/** + * @brief Interpolates two colors24bit and returns an color of the result + * + * @param color1 startcolor for interpolation + * @param color2 endcolor for interpolatio + * @param factor which color is wanted on the path from start to end color + * @return uint32_t interpolated color + */ +uint32_t LEDMatrix::interpolate_color_24bit(uint32_t color1, uint32_t color2, float factor) +{ + uint8_t resultRed = color1 >> 16 & 0xff; + uint8_t resultGreen = color1 >> 8 & 0xff; + uint8_t resultBlue = color1 & 0xff; + resultRed = (uint8_t)(resultRed + (int16_t)(factor * ((int16_t)(color2 >> 16 & 0xff) - (int16_t)resultRed))); + resultGreen = (uint8_t)(resultGreen + (int16_t)(factor * ((int16_t)(color2 >> 8 & 0xff) - (int16_t)resultGreen))); + resultBlue = (uint8_t)(resultBlue + (int16_t)(factor * ((int16_t)(color2 & 0xff) - (int16_t)resultBlue))); + return color_24bit(resultRed, resultGreen, resultBlue); +} + +/** + * @brief Setup function for LED matrix + * + */ +void LEDMatrix::setup_matrix() +{ + _neomatrix->begin(); + _neomatrix->setTextWrap(false); + _neomatrix->setBrightness(_brightness); + randomSeed(analogRead(0)); +} + +/** + * @brief Turn on the minutes indicator leds with the provided pattern (binary encoded) + * + * @param pattern the binary encoded pattern of the minute indicator + * @param color color to be displayed + */ +void LEDMatrix::set_min_indicator(uint8_t pattern, uint32_t color) +{ + // pattern: + // 15 -> 1111 + // 14 -> 1110 + // (...) + // 2 -> 0010 + // 1 -> 0001 + // 0 -> 0000 + if (pattern & 1) + { + _target_indicators[0] = color; + } + if (pattern >> 1 & 1) + { + _target_indicators[1] = color; + } + if (pattern >> 2 & 1) + { + _target_indicators[2] = color; + } + if (pattern >> 3 & 1) + { + _target_indicators[3] = color; + } +} + +/** + * @brief "Activates" a pixel in targetgrid with color + * + * @param x x-position of pixel + * @param y y-position of pixel + * @param color color of pixel + */ +void LEDMatrix::grid_add_pixel(uint8_t x, uint8_t y, uint32_t color) +{ + // limit ranges of x and y + if (x >= 0 && x < MATRIX_WIDTH && y >= 0 && y < MATRIX_HEIGHT) + { + _target_grid[y][x] = color; + } + else + { + // logger->log_string("Index out of Range: " + String(x) + ", " + String(y)); + } +} + +/** + * @brief "Deactivates" all pixels in targetgrid + * + */ +void LEDMatrix::flush(void) +{ + // set a zero to each pixel + for (uint8_t i = 0; i < MATRIX_HEIGHT; i++) + { + for (uint8_t j = 0; j < MATRIX_WIDTH; j++) + { + _target_grid[i][j] = 0; + } + } + // set every minutes indicator led to 0 + _target_indicators[0] = 0; + _target_indicators[1] = 0; + _target_indicators[2] = 0; + _target_indicators[3] = 0; +} + +/** + * @brief Write target pixels directly to leds + * + */ +void LEDMatrix::draw_on_matrix_instant() +{ + _draw_on_matrix(1.0); +} + +/** + * @brief Write target pixels with low pass filter to leds + * + * @param factor factor between 0 and 1 (1.0 = hard, 0.1 = smooth) + */ +void LEDMatrix::draw_on_matrix_smooth(float factor) +{ + _draw_on_matrix(factor); +} + +/** + * @brief Draws the targetgrid to the ledmatrix + * + * @param factor factor between 0 and 1 (1.0 = hard, 0.1 = smooth) + */ +void LEDMatrix::_draw_on_matrix(float factor) +{ + uint16_t total_current = 0; + uint32_t filtered_color = 0; + + // loop over all leds in matrix + for (int s = 0; s < MATRIX_WIDTH; s++) + { + for (int z = 0; z < MATRIX_HEIGHT; z++) + { + // inplement momentum as smooth transistion function + uint32_t filtered_color = interpolate_color_24bit(_current_grid[z][s], _target_grid[z][s], factor); + _neomatrix->drawPixel(s, z, color_24_to_16bit(filtered_color)); + _current_grid[z][s] = filtered_color; + total_current += _calc_estimated_led_current(filtered_color); + } + } + + // loop over all minute indicator leds + for (int i = 0; i < 4; i++) + { + filtered_color = interpolate_color_24bit(_current_indicators[i], _target_indicators[i], factor); + _neomatrix->drawPixel(MATRIX_WIDTH - (1 + i), MATRIX_HEIGHT, color_24_to_16bit(filtered_color)); + _current_indicators[i] = filtered_color; + total_current += _calc_estimated_led_current(filtered_color); + } + + // Check if totalCurrent reaches CURRENTLIMIT -> if yes reduce brightness + if (total_current > _current_limit) + { + uint8_t new_brightness = _brightness * float(_current_limit) / float(total_current); + _neomatrix->setBrightness(new_brightness); + } + _neomatrix->show(); +} + +/** + * @brief Shows a 1-digit number on LED matrix (5x3) + * + * @param xpos x of left top corner of digit + * @param ypos y of left top corner of digit + * @param number number to display + * @param color color to display (24bit) + */ +void LEDMatrix::print_number(uint8_t xpos, uint8_t ypos, uint8_t number, uint32_t color) +{ + for (int y = ypos, i = 0; y < (ypos + 5); y++, i++) + { + for (int x = xpos, k = 2; x < (xpos + 3); x++, k--) + { + if ((numbers_font[number][i] >> k) & 0x1) + { + grid_add_pixel(x, y, color); + } + } + } +} + +/** + * @brief Shows a character on LED matrix (5x3), supports currently only 'I' and 'P' + * + * @param xpos x of left top corner of character + * @param ypos y of left top corner of character + * @param character character to display + * @param color color to display (24bit) + */ +void LEDMatrix::print_char(uint8_t xpos, uint8_t ypos, char character, uint32_t color) +{ + int id = 0; + if (character == 'I') + { + id = 0; + } + else if (character == 'P') + { + id = 1; + } + + for (int y = ypos, i = 0; y < (ypos + 5); y++, i++) + { + for (int x = xpos, k = 2; x < (xpos + 3); x++, k--) + { + if ((chars_font[id][i] >> k) & 0x1) + { + grid_add_pixel(x, y, color); + } + } + } +} + +/** + * @brief Set Brightness + * + * @param mybrightness brightness to be set [0..255] + */ +void LEDMatrix::set_brightness(uint8_t brightness) +{ + _brightness = brightness; + _neomatrix->setBrightness(_brightness); +} + +/** + * @brief Calc estimated current (mA) for one pixel with the given color and brightness + * + * @param color 24bit color value of the pixel for which the current should be calculated + * @return the current in mA + */ +uint16_t LEDMatrix::_calc_estimated_led_current(uint32_t color) +{ + // extract rgb values + uint8_t red = color >> 16 & 0xff; + uint8_t green = color >> 8 & 0xff; + uint8_t blue = color & 0xff; + + // Linear estimation: 20mA for full brightness per LED + // (calculation avoids float numbers) + uint32_t estimated_current = (MAX_LED_CURRENT_MA * red) + (MAX_LED_CURRENT_MA * green) + (MAX_LED_CURRENT_MA * blue); + estimated_current /= UINT8_MAX; + estimated_current = (estimated_current * _brightness) / UINT8_MAX; + + return estimated_current; +} + +/** + * @brief Set the current limit + * + * @param new_current_limit the total current limit for whole matrix + */ +void LEDMatrix::set_current_limit(uint16_t new_current_limit) +{ + _current_limit = new_current_limit; +} \ No newline at end of file diff --git a/src/wordclock_esp8266.cpp b/src/wordclock_esp8266.cpp new file mode 100644 index 0000000..4a98e44 --- /dev/null +++ b/src/wordclock_esp8266.cpp @@ -0,0 +1,1064 @@ +/** + * Wordclock 2.0 - Wordclock with ESP8266 and NTP time update + * + * created by techniccontroller 04.12.2021 + * + * components: + * - ESP8266 + * - Neopixelstrip + * + * Board settings: + * - Board: NodeMCU 1.0 (ESP-12E Module) + * - Flash Size: 4MB (FS:2MB OTA:~1019KB) + * - Upload Speed: 115200 + * + * + * with code parts from: + * - Adafruit NeoPixel strandtest.ino, https://github.com/adafruit/Adafruit_NeoPixel/blob/master/examples/strandtest/strandtest.ino + * - Esp8266 und Esp32 webserver https://fipsok.de/ + * - https://github.com/pmerlin/PMR-LED-Table/blob/master/tetrisGame.ino + * - https://randomnerdtutorials.com/wifimanager-with-esp8266-autoconnect-custom-parameter-and-manage-your-ssid-and-password/ + * + */ +#include +#include "wordclock_esp8266.h" + +#include // https://github.com/adafruit/Adafruit-GFX-Library +#include // https://github.com/adafruit/Adafruit_NeoMatrix +#include // NeoPixel library used to run the NeoPixel LEDs: https://github.com/adafruit/Adafruit_NeoPixel +#include //from ESP8266 Arduino Core (automatically installed when ESP8266 was installed via Boardmanager) +#include +#include +#include //https://github.com/tzapu/WiFiManager WiFi Configuration Magic +#include + +// own libraries +#include "animationfunctions.h" +#include "pong.h" +#include "snake.h" +#include "tetris.h" +#include "ledmatrix.h" +#include "littlefs_wrapper.h" +#include "base64_wrapper.h" // copied from https://github.com/Xander-Electronics/Base64 +#include "ntp_client_plus.h" +#include "otafunctions.h" +#include "udp_logger.h" +#include "wordclock_constants.h" +#include "wordclock_functions.h" + +// DEBUG +uint32_t dbg_counter = 0; // TODO RM + +const String state_names[] = {"Clock", "DiClock", "Spiral", "Tetris", "Snake", "PingPong"}; +// PERIODS for each state (different for stateAutoChange or Manual mode) +const uint16_t PERIODS[2][NUM_STATES] = {{PERIOD_TIME_VISU_UPDATE, // stateAutoChange = 0 + PERIOD_TIME_VISU_UPDATE, + PERIOD_ANIMATION, + PERIOD_TETRIS, + PERIOD_SNAKE, + PERIOD_PONG}, + {PERIOD_TIME_VISU_UPDATE, // stateAutoChange = 1 + PERIOD_TIME_VISU_UPDATE, + PERIOD_ANIMATION, + PERIOD_ANIMATION, + PERIOD_ANIMATION, + PERIOD_PONG}}; + +// ---------------------------------------------------------------------------------- +// GLOBAL VARIABLES +// ---------------------------------------------------------------------------------- + +// Webserver +ESP8266WebServer webserver(HTTP_PORT); + +// When we setup the NeoPixel library, we tell it how many pixels, and which pin to use to send signals. +// Note that for older NeoPixel strips you might need to change the third parameter--see the strandtest +// example for more information on possible values. +Adafruit_NeoMatrix matrix = Adafruit_NeoMatrix(MATRIX_WIDTH, MATRIX_HEIGHT + 1, NEOPIXEL_PIN, + NEO_MATRIX_TOP + NEO_MATRIX_LEFT + NEO_MATRIX_ROWS + NEO_MATRIX_ZIGZAG, + NEO_GRB + NEO_KHZ800); + +// Globals +uint8_t brightness = 40; // current brightness of LEDs +LEDMatrix led_matrix = LEDMatrix(&matrix, brightness, &logger); +UDPLogger logger; + +// Statics +static bool spiral_direction = false; + +static WiFiUDP wifi_udp; +static NTPClientPlus ntp_client = NTPClientPlus(wifi_udp, NTP_SERVER_URL, 1, true); +static Pong pong = Pong(&led_matrix, &logger); +static Snake snake = Snake(&led_matrix, &logger); +static Tetris tetris = Tetris(&led_matrix, &logger); + +// Timestamp variables +static unsigned long button_press_start = 0; // time of push button press start +static unsigned long last_led_direct = 0; // time of last direct LED command (=> fall back to normal mode after timeout) +static unsigned long last_animation_step = millis(); // time of last Matrix update +static unsigned long last_heartbeat = millis(); // time of last heartbeat sending +static unsigned long last_nightmode_check = millis(); // time of last nightmode check +static unsigned long last_ntp_update = millis() - PERIOD_NTP_UPDATE - 5000; // time of last NTP update +static unsigned long last_state_change = millis(); // time of last state change +static unsigned long last_step = millis(); // time of last animation step + +static bool night_mode = false; // stores state of nightmode +static bool state_auto_change = false; // stores state of automatic state change +static float filter_factor = DEFAULT_SMOOTHING_FACTOR; // stores smoothing factor for led transition +static uint32_t maincolor_clock = colors_24bit[2]; // color of the clock and digital clock +static uint32_t maincolor_snake = colors_24bit[1]; // color of the random snake animation +static uint8_t current_state = (uint8_t)ST_CLOCK; // stores current state +static int watchdog_counter = 30; // Watchdog counter to trigger restart if NTP update was not possible 30 times in a row (5min) + +// Nightmode time settings +NightModeTimes_st night_mode_times = { + NIGHTMODE_START_HR, + NIGHTMODE_START_MIN, + NIGHTMODE_END_HR, + NIGHTMODE_END_MIN}; + +// ---------------------------------------------------------------------------------- +// SETUP +// ---------------------------------------------------------------------------------- + +void setup() +{ + // put your setup code here, to run once: + Serial.begin(115200); + delay(100); + Serial.println(); + Serial.printf("\nSketchname: %s\nBuild: %s\n", (__FILE__), (__TIMESTAMP__)); + Serial.println(); + + // Reset info + rst_info *resetInfo = ESP.getResetInfoPtr(); + Serial.printf("Reset reason: %u\n", resetInfo->reason); + Serial.printf("Reset cause: %u\n", resetInfo->exccause); + Serial.printf("Reset address: %u\n", resetInfo->excvaddr); + Serial.println(); + + // Init EEPROM + EEPROM.begin(EEPROM_SIZE); + + // Load color for clock from EEPROM + load_main_color(); + + // configure button pin as input + pinMode(BUTTON_PIN, INPUT_PULLUP); + + // setup Matrix LED functions + led_matrix.setup_matrix(); + led_matrix.set_current_limit(CURRENT_LIMIT_LED); + + // Turn on minutes leds (blue) + led_matrix.set_min_indicator(15, colors_24bit[6]); + led_matrix.draw_on_matrix_instant(); + + /** Use WiFiMaanger for handling initial Wifi setup **/ + + // Local intialization. Once its business is done, there is no need to keep it around + WiFiManager wifiManager; + + // fetches ssid and pass from eeprom and tries to connect + // if it does not connect it starts an access point with the specified name + // here "wordclockAP" + // and goes into a blocking loop awaiting configuration + wifiManager.autoConnect(AP_SSID); + + // if you get here you have connected to the WiFi + Serial.printf("Connected, IP address: "); + Serial.println(WiFi.localIP()); + + // Turn off minutes leds + led_matrix.set_min_indicator(15, 0); + led_matrix.draw_on_matrix_instant(); + + // init ESP8266 File manager (LittleFS) + setup_filesystem(); + + // setup OTA + setupOTA(HOSTNAME); + + webserver.on("/cmd", handle_command); // process commands + webserver.on("/data", handle_data_request); // process datarequests + webserver.on("/leddirect", HTTP_POST, handle_led_direct); // Call the 'handle_led_direct' function when a POST request is made to URI "/leddirect" + webserver.begin(); + + // create UDP Logger to send logging messages via UDP multicast + logger = UDPLogger(WiFi.localIP(), LOGGER_MULTICAST_IP, LOGGER_MULTICAST_PORT, "Wordclock 2.0"); + logger.log_string("Start program\n"); + delay(10); + logger.log_string("Sketchname: " + String(__FILE__)); + delay(10); + logger.log_string("Build: " + String(__TIMESTAMP__)); + delay(10); + logger.log_string("IP: " + WiFi.localIP().toString()); + delay(10); + logger.log_string("Reset Reason: " + ESP.getResetReason()); + + if (resetInfo->reason != REASON_SOFT_RESTART) + { + // test quickly each LED + for (int r = 0; r < MATRIX_HEIGHT; r++) + { + for (int c = 0; c < MATRIX_WIDTH; c++) + { + matrix.fillScreen(0); + matrix.drawPixel(c, r, LEDMatrix::color_24_to_16bit(colors_24bit[2])); + matrix.show(); + delay(10); + } + } + + // clear Matrix + matrix.fillScreen(0); + matrix.show(); + delay(200); + + // display IP + uint8_t address = WiFi.localIP()[3]; + led_matrix.print_char(1, 0, 'I', maincolor_clock); + led_matrix.print_char(5, 0, 'P', maincolor_clock); + led_matrix.print_number(0, 6, (address / 100), maincolor_clock); + led_matrix.print_number(4, 6, (address / 10) % 10, maincolor_clock); + led_matrix.print_number(8, 6, address % 10, maincolor_clock); + led_matrix.draw_on_matrix_instant(); + delay(2000); + + // clear matrix + led_matrix.flush(); + led_matrix.draw_on_matrix_instant(); + } + // setup NTP + ntp_client.setupNTPClient(); + logger.log_string("NTP running"); + logger.log_string("Time: " + ntp_client.getFormattedTime()); + logger.log_string("TimeOffset (seconds): " + String(ntp_client.getTimeOffset())); + + // show the current time for short time in words + int hours = ntp_client.getHours24(); + int minutes = ntp_client.getMinutes(); + String timeMessage = time_to_string(hours, minutes); + show_string_on_clock(timeMessage, maincolor_clock); + draw_minute_indicator(minutes, maincolor_clock); + led_matrix.draw_on_matrix_smooth(filter_factor); + + // init all animation modes + // init snake + random_snake(true, 8, colors_24bit[1], -1); + // init spiral + draw_spiral(true, spiral_direction, MATRIX_WIDTH - 6); + // init random tetris + random_tetris(true); + + // Read nightmode setting from EEPROM + night_mode_times.nightmode_start_hour = EEPROM_read_address(ADR_NM_START_H); + night_mode_times.nightmode_start_min = EEPROM_read_address(ADR_NM_START_M); + night_mode_times.nightmode_end_hour = EEPROM_read_address(ADR_NM_END_H); + night_mode_times.nightmode_end_min = EEPROM_read_address(ADR_NM_END_M); + + if (night_mode_times.nightmode_start_hour < 0 || night_mode_times.nightmode_start_hour > 23) + { + night_mode_times.nightmode_start_hour = NIGHTMODE_START_HR; + } + if (night_mode_times.nightmode_start_min < 0 || night_mode_times.nightmode_start_min > 59) + { + night_mode_times.nightmode_start_min = NIGHTMODE_START_MIN; + } + if (night_mode_times.nightmode_end_hour < 0 || night_mode_times.nightmode_end_hour > 23) + { + night_mode_times.nightmode_end_hour = NIGHTMODE_END_HR; + } + if (night_mode_times.nightmode_end_min < 0 || night_mode_times.nightmode_end_min > 59) + { + night_mode_times.nightmode_end_min = NIGHTMODE_END_MIN; + } + logger.log_string("Nightmode starts at: " + String(night_mode_times.nightmode_start_hour) + ":" + String(night_mode_times.nightmode_start_min)); + logger.log_string("Nightmode ends at: " + String(night_mode_times.nightmode_end_hour) + ":" + String(night_mode_times.nightmode_end_min)); + + // Read brightness setting from EEPROM, lower limit is 10 so that the LEDs are not completely off + brightness = EEPROM_read_address(ADR_BRIGHTNESS); + if (brightness < 10) + { + brightness = 10; + } + logger.log_string("Brightness: " + String(brightness)); + led_matrix.set_brightness(brightness); +} + +// ---------------------------------------------------------------------------------- +// LOOP +// ---------------------------------------------------------------------------------- + +void loop() +{ + handleOTA(); // handle OTA + logger.log_string("After handleOTA"); + + webserver.handleClient(); // handle webserver + logger.log_string("After handleClient"); + + send_heartbeat(); // send heartbeat update + logger.log_string("After send_heartbeat"); + + handle_current_state(); // handle current state - main process + logger.log_string("After handle_current_state"); + + update_matrix(); // update matrix + logger.log_string("After update_matrix"); + + handle_button(); // handle button press + logger.log_string("After handle_button"); + + update_state_machine(); // handle state changes + logger.log_string("After update_state_machine"); + + ntp_time_update(); // ntp time update + logger.log_string("After ntp_time_update"); + + check_night_mode(); // check night mode + logger.log_string("After check_night_mode"); +} + +// ---------------------------------------------------------------------------------- +// OTHER FUNCTIONS +// ---------------------------------------------------------------------------------- + +void update_state_machine() +{ + if (state_auto_change && (millis() - last_state_change > PERIOD_STATE_CHANGE) && !night_mode) + { + // increment state variable and trigger state change + state_change((current_state + 1) % NUM_STATES); + // save last automatic state change + last_state_change = millis(); + } +} + +void handle_current_state() +{ + // handle mode behaviours (trigger loopCycles of different modes depending on current mode) + if (!night_mode && (millis() - last_step > PERIODS[state_auto_change][current_state]) && (millis() - last_led_direct > TIMEOUT_LEDDIRECT)) + { + switch (current_state) + { + case ST_CLOCK: // state clock + { + int hours = ntp_client.getHours24(); + int minutes = ntp_client.getMinutes(); + + (void)show_string_on_clock(time_to_string((uint8_t)hours, (uint8_t)minutes), maincolor_clock); + draw_minute_indicator((uint8_t)minutes, maincolor_clock); + break; + } + case ST_DICLOCK: // state diclock + { + int hours = ntp_client.getHours24(); + int minutes = ntp_client.getMinutes(); + show_digital_clock((uint8_t)hours, (uint8_t)minutes, maincolor_clock); + break; + } + case ST_SPIRAL: // state spiral + { + int res = draw_spiral(false, spiral_direction, MATRIX_WIDTH - 6); + if (res && spiral_direction == 0) + { + // change spiral direction to closing (draw empty leds) + spiral_direction = 1; + // init spiral with new spiral direction + draw_spiral(true, spiral_direction, MATRIX_WIDTH - 6); + } + else if (res && spiral_direction == 1) + { + // reset spiral direction to normal drawing leds + spiral_direction = 0; + // init spiral with new spiral direction + draw_spiral(true, spiral_direction, MATRIX_WIDTH - 6); + } + break; + } + case ST_TETRIS: // state tetris + { + if (state_auto_change) + { + random_tetris(false); + } + else + { + tetris.loopCycle(); + } + break; + } + case ST_SNAKE: // state snake + { + if (state_auto_change) + { + led_matrix.flush(); + if (random_snake(false, 8, maincolor_snake, -1)) + { + // init snake for next run + random_snake(true, 8, maincolor_snake, -1); + } + } + else + { + snake.loopCycle(); + } + break; + } + case ST_PINGPONG: // state ping pong + { + pong.loopCycle(); + break; + } + default: + { + break; + } + } + last_step = millis(); + } +} + +/** + * @brief Update matrix colors, should be called in loop() + * + * @param None + */ +void update_matrix() +{ + // periodically write colors to matrix + if (millis() - last_animation_step > PERIOD_MATRIX_UPDATE) + { + led_matrix.draw_on_matrix_smooth(filter_factor); + last_animation_step = millis(); + } +} + +/** + * @brief Send heartbeat, should be called in loop() + * + * @param None + */ +void send_heartbeat() +{ + // send regularly heartbeat messages via UDP multicast + if (millis() - last_heartbeat > PERIOD_HEARTBEAT) + { + logger.log_string("Heartbeat, state: " + state_names[current_state] + ", FreeHeap: " + ESP.getFreeHeap() + ", HeapFrag: " + ESP.getHeapFragmentation() + ", MaxFreeBlock: " + ESP.getMaxFreeBlockSize() + "\nCounter: " + dbg_counter + " , Hours: " + (float)(dbg_counter) / 3600.0 + "\n"); // TODO CHANGE + last_heartbeat = millis(); + + // Check wifi status (only if no apmode) + if (WiFi.status() != WL_CONNECTED) + { + Serial.println("WiFi connection lost!"); + led_matrix.grid_add_pixel(0, 5, colors_24bit[1]); + led_matrix.draw_on_matrix_instant(); + } + dbg_counter++; // TODO RM + } +} + +/** + * @brief Night mode check, should be called in loop() + * + * @param None + */ +void check_night_mode() +{ + // check if nightmode need to be activated + if (millis() - last_nightmode_check > PERIOD_NIGHTMODE_CHECK) + { + int hours = ntp_client.getHours24(); + int minutes = ntp_client.getMinutes(); + + if (hours == night_mode_times.nightmode_start_hour && minutes == night_mode_times.nightmode_start_min) + { + set_night_mode(true); + } + else if (hours == night_mode_times.nightmode_end_hour && minutes == night_mode_times.nightmode_end_min) + { + set_night_mode(false); + } + + last_nightmode_check = millis(); + } +} + +/** + * @brief NTP time update, should be called in loop() + * + * @param None + */ +void ntp_time_update() +{ + // NTP time update + if (millis() - last_ntp_update > PERIOD_NTP_UPDATE) + { + int ntp_retval = ntp_client.updateNTP(); + switch (ntp_retval) + { + case NTP_UPDATE_SUCCESS: + { + ntp_client.calcDate(); + logger.log_string("NTP-Update successful"); + logger.log_string("Time: " + ntp_client.getFormattedTime()); + logger.log_string("Date: " + ntp_client.getFormattedDate()); + logger.log_string("TimeOffset (seconds): " + String(ntp_client.getTimeOffset())); + logger.log_string("Summertime: " + String(ntp_client.updateSWChange())); + last_ntp_update = millis(); + watchdog_counter = 30; + break; + } + case NTP_UPDATE_TIMEOUT: + { + logger.log_string("NTP-Update not successful. Reason: Timeout"); + last_ntp_update += 10000; + watchdog_counter--; + break; + } + case NTP_UPDATE_DIFFTOOHIGH: + { + logger.log_string("NTP-Update not successful. Reason: Too large time difference"); + logger.log_string("Time: " + ntp_client.getFormattedTime()); + logger.log_string("Date: " + ntp_client.getFormattedDate()); + logger.log_string("TimeOffset (seconds): " + String(ntp_client.getTimeOffset())); + logger.log_string("Summertime: " + String(ntp_client.updateSWChange())); + last_ntp_update += 10000; + watchdog_counter--; + break; + } + case NTP_UPDATE_TIME_INVALID: + default: + { + logger.log_string("NTP-Update not successful. Reason: NTP time not valid (<1970)"); + last_ntp_update += 10000; + watchdog_counter--; + break; + } + } + + logger.log_string("Watchdog Counter: " + String(watchdog_counter)); + if (watchdog_counter <= 0) + { + logger.log_string("Trigger restart due to watchdog..."); + delay(100); + ESP.restart(); + } + } +} + +/** + * @brief call entry action of given state + * + * @param state + */ +void on_state_entry(uint8_t state) +{ + filter_factor = 0.5; + switch (state) + { + case ST_SPIRAL: + { // Init spiral with normal drawing mode + spiral_direction = 0; + draw_spiral(true, spiral_direction, MATRIX_WIDTH - 6); + break; + } + case ST_TETRIS: + { + filter_factor = 1.0; // no smoothing + if (state_auto_change) + { + random_tetris(true); + } + else + { + tetris.ctrlStart(); + } + break; + } + case ST_SNAKE: + { + if (state_auto_change) + { + random_snake(true, 8, colors_24bit[1], -1); + } + else + { + filter_factor = 1.0; // no smoothing + snake.initGame(); + } + break; + } + case ST_PINGPONG: + { + if (state_auto_change) + { + pong.initGame(2); + } + else + { + filter_factor = 1.0; // no smoothing + pong.initGame(1); + } + break; + } + default: + { + break; + } + } +} + +/** + * @brief execute a state change to given newState + * + * @param newState the new state to be changed to + */ +void state_change(uint8_t newState) +{ + if (night_mode) + { + // deactivate Nightmode + set_night_mode(false); + } + // first clear matrix + led_matrix.flush(); + // set new state + current_state = newState; + on_state_entry(current_state); + logger.log_string("State change to: " + state_names[current_state]); + delay(5); + logger.log_string("FreeMemory=" + String(ESP.getFreeHeap())); +} + +/** + * @brief Handler for POST requests to /leddirect. + * + * Allows the control of all LEDs from external source. + * It will overwrite the normal program for 5 seconds. + * A 11x11 picture can be sent as base64 encoded string to be displayed on matrix. + * + */ +void handle_led_direct() +{ + if (webserver.method() != HTTP_POST) + { + webserver.send(405, "text/plain", "Method Not Allowed"); + } + else + { + String message = "POST data was:\n"; + if (webserver.args() == 1) + { + String data = String(webserver.arg(0)); + unsigned int dataLength = data.length(); + + // base64 decoding + char base64data[dataLength]; + data.toCharArray(base64data, dataLength); + int base64dataLen = (int)dataLength; + int decodedLength = Base64.decodedLength(base64data, base64dataLen); + char byteArray[decodedLength]; + Base64.decode(byteArray, base64data, base64dataLen); + + for (unsigned int i = 0; i < dataLength; i += 4) + { + uint8_t red = byteArray[i]; // red + uint8_t green = byteArray[i + 1]; // green + uint8_t blue = byteArray[i + 2]; // blue + led_matrix.grid_add_pixel((i / 4) % MATRIX_WIDTH, (i / 4) / MATRIX_HEIGHT, LEDMatrix::color_24bit(red, green, blue)); + } + led_matrix.draw_on_matrix_instant(); + + last_led_direct = millis(); + } + webserver.send(200, "text/plain", message); + } +} + +/** + * @brief Check button commands + * + */ +void handle_button() +{ + static bool lastButtonState = false; + bool buttonPressed = !digitalRead(BUTTON_PIN); + // check rising edge + if (buttonPressed == true && lastButtonState == false) + { + // button press start + logger.log_string("Button press started"); + button_press_start = millis(); + } + // check falling edge + if (buttonPressed == false && lastButtonState == true) + { + // button press ended + if ((millis() - button_press_start) > LONG_PRESS_MS) + { + // longpress -> nightmode + logger.log_string("Button press ended - long press"); + + set_night_mode(true); + } + else if ((millis() - button_press_start) > SHORT_PRESS_MS) + { + // shortpress -> state change + logger.log_string("Button press ended - short press"); + + if (night_mode) + { + set_night_mode(false); + } + else + { + state_change((current_state + 1) % NUM_STATES); + } + } + } + lastButtonState = buttonPressed; +} + +/** + * @brief Set main color + * + */ + +void set_main_color(uint8_t red, uint8_t green, uint8_t blue) +{ + maincolor_clock = LEDMatrix::color_24bit(red, green, blue); + EEPROM.put(ADR_MC_RED, red); + EEPROM.put(ADR_MC_GREEN, green); + EEPROM.put(ADR_MC_BLUE, blue); + EEPROM.commit(); +} + +/** + * @brief Load maincolor from EEPROM + * + */ + +void load_main_color() +{ + uint8_t red = EEPROM.read(ADR_MC_RED); + uint8_t green = EEPROM.read(ADR_MC_GREEN); + uint8_t blue = EEPROM.read(ADR_MC_BLUE); + if (int(red) + int(green) + int(blue) < 50) + { + maincolor_clock = colors_24bit[2]; + } + else + { + maincolor_clock = LEDMatrix::color_24bit(red, green, blue); + } +} + +/** + * @brief Handler for handling commands sent to "/cmd" url + * + */ +void handle_command() +{ + // receive command and handle accordingly + for (uint8_t i = 0; i < webserver.args(); i++) + { + Serial.print(webserver.argName(i)); + Serial.print(F(": ")); + Serial.println(webserver.arg(i)); + } + + if (webserver.argName(0).equals("led")) // the parameter which was sent to this server is led color + { + String color_str = webserver.arg(0) + "-"; + String red_str = split(color_str, '-', 0); + String green_str = split(color_str, '-', 1); + String blue_str = split(color_str, '-', 2); + logger.log_string(color_str); + logger.log_string("r: " + String(red_str.toInt())); + logger.log_string("g: " + String(green_str.toInt())); + logger.log_string("b: " + String(blue_str.toInt())); + // set new main color + set_main_color(red_str.toInt(), green_str.toInt(), blue_str.toInt()); + } + else if (webserver.argName(0).equals("mode")) // the parameter which was sent to this server is mode change + { + String mode_str = webserver.arg(0); + logger.log_string("Mode change via Webserver to: " + mode_str); + // set current mode/state accordant sent mode + if (mode_str.equals("clock")) + { + state_change(ST_CLOCK); + } + else if (mode_str.equals("diclock")) + { + state_change(ST_DICLOCK); + } + else if (mode_str.equals("spiral")) + { + state_change(ST_SPIRAL); + } + else if (mode_str.equals("tetris")) + { + state_change(ST_TETRIS); + } + else if (mode_str.equals("snake")) + { + state_change(ST_SNAKE); + } + else if (mode_str.equals("pingpong")) + { + state_change(ST_PINGPONG); + } + } + else if (webserver.argName(0).equals("nightmode")) + { + String mode_str = webserver.arg(0); + logger.log_string("Nightmode change via Webserver to: " + mode_str); + mode_str.equals("1") ? set_night_mode(true) : set_night_mode(false); + } + else if (webserver.argName(0).equals("setting")) + { + String time_str = webserver.arg(0) + "-"; + logger.log_string("Nightmode setting change via Webserver to: " + time_str); + night_mode_times.nightmode_start_hour = split(time_str, '-', 0).toInt(); + night_mode_times.nightmode_start_min = split(time_str, '-', 1).toInt(); + night_mode_times.nightmode_end_hour = split(time_str, '-', 2).toInt(); + night_mode_times.nightmode_end_min = split(time_str, '-', 3).toInt(); + brightness = split(time_str, '-', 4).toInt(); + if (brightness < 10) + { + brightness = 10; + } + if (night_mode_times.nightmode_start_hour < 0 || night_mode_times.nightmode_start_hour > 23) + { + night_mode_times.nightmode_start_hour = NIGHTMODE_START_HR; + } + if (night_mode_times.nightmode_start_min < 0 || night_mode_times.nightmode_start_min > 59) + { + night_mode_times.nightmode_start_min = NIGHTMODE_START_MIN; + } + if (night_mode_times.nightmode_end_hour < 0 || night_mode_times.nightmode_end_hour > 23) + { + night_mode_times.nightmode_end_hour = NIGHTMODE_END_HR; + } + if (night_mode_times.nightmode_end_min < 0 || night_mode_times.nightmode_end_min > 59) + { + night_mode_times.nightmode_end_min = NIGHTMODE_END_MIN; + } + + EEPROM_write_to_address(ADR_NM_START_H, night_mode_times.nightmode_start_hour); + EEPROM_write_to_address(ADR_NM_START_M, night_mode_times.nightmode_start_min); + EEPROM_write_to_address(ADR_NM_END_H, night_mode_times.nightmode_end_hour); + EEPROM_write_to_address(ADR_NM_END_M, night_mode_times.nightmode_end_min); + EEPROM_write_to_address(ADR_BRIGHTNESS, brightness); + + logger.log_string("Nightmode starts at: " + String(night_mode_times.nightmode_start_hour) + ":" + String(night_mode_times.nightmode_start_min)); + logger.log_string("Nightmode ends at: " + String(night_mode_times.nightmode_end_hour) + ":" + String(night_mode_times.nightmode_end_min)); + logger.log_string("Brightness: " + String(brightness)); + led_matrix.set_brightness(brightness); + } + else if (webserver.argName(0).equals("stateautochange")) + { + String mode_str = webserver.arg(0); + logger.log_string("stateAutoChange change via Webserver to: " + mode_str); + mode_str.equals("1") ? state_auto_change = true : state_auto_change = false; + } + else if (webserver.argName(0).equals("tetris")) + { + String cmd_str = webserver.arg(0); + logger.log_string("Tetris cmd via Webserver to: " + cmd_str); + if (cmd_str.equals("up")) + { + tetris.ctrlUp(); + } + else if (cmd_str.equals("left")) + { + tetris.ctrlLeft(); + } + else if (cmd_str.equals("right")) + { + tetris.ctrlRight(); + } + else if (cmd_str.equals("down")) + { + tetris.ctrlDown(); + } + else if (cmd_str.equals("play")) + { + tetris.ctrlStart(); + } + else if (cmd_str.equals("pause")) + { + tetris.ctrlPlayPause(); + } + } + else if (webserver.argName(0).equals("snake")) + { + String cmd_str = webserver.arg(0); + logger.log_string("Snake cmd via Webserver to: " + cmd_str); + if (cmd_str.equals("up")) + { + snake.ctrlUp(); + } + else if (cmd_str.equals("left")) + { + snake.ctrlLeft(); + } + else if (cmd_str.equals("right")) + { + snake.ctrlRight(); + } + else if (cmd_str.equals("down")) + { + snake.ctrlDown(); + } + else if (cmd_str.equals("new")) + { + snake.initGame(); + } + } + else if (webserver.argName(0).equals("pong")) + { + String cmd_str = webserver.arg(0); + logger.log_string("Pong cmd via Webserver to: " + cmd_str); + if (cmd_str.equals("up")) + { + pong.ctrlUp(1); + } + else if (cmd_str.equals("down")) + { + pong.ctrlDown(1); + } + else if (cmd_str.equals("new")) + { + pong.initGame(1); + } + } + webserver.send(204, "text/plain", "No Content"); // this page doesn't send back content --> 204 +} + +/** + * @brief Splits a string at given character and return specified element + * + * @param s string to split + * @param parser separating character + * @param index index of the element to return + * @return String + */ +String split(String s, char parser, int index) +{ + String rs = ""; + int parser_cnt = 0; + int r_from_index = 0, r_to_index = -1; + while (index >= parser_cnt) + { + r_from_index = r_to_index + 1; + r_to_index = s.indexOf(parser, r_from_index); + if (index == parser_cnt) + { + if (r_to_index == 0 || r_to_index == -1) + { + return ""; + } + return s.substring(r_from_index, r_to_index); + } + else + parser_cnt++; + } + return rs; +} + +/** + * @brief Handler for GET requests + * + */ +void handle_data_request() +{ + // receive data request and handle accordingly + for (uint8_t i = 0; i < webserver.args(); i++) + { + Serial.print(webserver.argName(i)); + Serial.print(F(": ")); + Serial.println(webserver.arg(i)); + } + + if (webserver.argName(0).equals("key")) // the parameter which was sent to this server is led color + { + String message = "{"; + String keystr = webserver.arg(0); + if (keystr.equals("mode")) + { + message += "\"mode\":\"" + state_names[current_state] + "\""; + message += ","; + message += "\"modeid\":\"" + String(current_state) + "\""; + message += ","; + message += "\"stateAutoChange\":\"" + String(state_auto_change) + "\""; + message += ","; + message += "\"night_mode\":\"" + String(night_mode) + "\""; + message += ","; + message += "\"nightModeStart\":\"" + leading_zero2digit(night_mode_times.nightmode_start_hour) + "-" + leading_zero2digit(night_mode_times.nightmode_start_min) + "\""; + message += ","; + message += "\"nightModeEnd\":\"" + leading_zero2digit(night_mode_times.nightmode_end_hour) + "-" + leading_zero2digit(night_mode_times.nightmode_end_min) + "\""; + message += ","; + message += "\"brightness\":\"" + String(brightness) + "\""; + } + message += "}"; + webserver.send(200, "application/json", message); + } +} + +/** + * @brief Set the nightmode state + * + * @param state true -> nightmode on + */ +void set_night_mode(bool state) +{ + led_matrix.flush(); + led_matrix.draw_on_matrix_smooth(0.2); + night_mode = state; +} + +/** + * @brief Write value to EEPROM + * + * @param address address to write the value + * @param value value to write + */ +void EEPROM_write_to_address(int address, int value) +{ + EEPROM.put(address, value); + EEPROM.commit(); +} + +/** + * @brief Read value from EEPROM + * + * @param address address + * @return int value + */ +int EEPROM_read_address(int address) +{ + int value; + EEPROM.get(address, value); + return value; +} + +/** + * @brief Convert Integer to String with leading zero + * + * @param value + * @return String + */ +String leading_zero2digit(int value) +{ + String msg = ""; + if (value < 10) + { + msg = "0"; + } + msg += String(value); + return msg; +} diff --git a/src/wordclock_functions.cpp b/src/wordclock_functions.cpp new file mode 100644 index 0000000..b550c53 --- /dev/null +++ b/src/wordclock_functions.cpp @@ -0,0 +1,238 @@ +#include +#include "wordclock_functions.h" +#include "ledmatrix.h" +#include "udp_logger.h" + + +extern LEDMatrix led_matrix; +extern String split(String s, char parser, int index); // TODO cleanup + +const String clockStringGerman = "ESPISTAFUNFVIERTELZEHNZWANZIGUVORTECHNICNACHHALBMELFUNFXCONTROLLEREINSEAWZWEIDREITUMVIERSECHSQYACHTSIEBENZWOLFZEHNEUNJUHR"; + +/** + * @brief control the four minute indicator LEDs + * + * @param minutes minutes to be displayed [0 ... 59] + * @param color 24bit color value + */ +void draw_minute_indicator(uint8_t minutes, uint32_t color) +{ + // separate LEDs for minutes in an additional row + { + switch (minutes % 5) + { + case 1: + { + led_matrix.set_min_indicator(0b1000, color); + break; + } + + case 2: + { + led_matrix.set_min_indicator(0b1100, color); + break; + } + + case 3: + { + led_matrix.set_min_indicator(0b1110, color); + break; + } + + case 4: + { + led_matrix.set_min_indicator(0b1111, color); + break; + } + case 0: + default: + { + break; + } + } + } +} + +/** + * @brief Draw the given sentence to the word clock + * + * @param message sentence to be displayed + * @param color 24bit color value + * @return int: 0 if successful, -1 if sentence not possible to display + */ +int show_string_on_clock(String message, uint32_t color) +{ + String word = ""; + int last_letter_clock = 0; + int word_position = 0; + int idx = 0; + + // add space on the end of message for splitting + message = message + " "; + + // empty the target grid + led_matrix.flush(); + + while (true) + { + // extract next word from message + word = split(message, ' ', idx); + idx++; + + if (word.length() > 0) + { + // find word in clock string + word_position = clockStringGerman.indexOf(word, last_letter_clock); + + if (word_position >= 0) + { + // word found on clock -> enable leds in targetgrid + for (unsigned int i = 0; i < word.length(); i++) + { + unsigned int x = (word_position + i) % MATRIX_WIDTH; + unsigned int y = (word_position + i) / MATRIX_WIDTH; + led_matrix.grid_add_pixel(x, y, color); + } + // remember end of the word on clock + last_letter_clock = word_position + word.length(); + } + else + { + // word is not possible to show on clock + return -1; + } + } + else // end - no more word in message + { + + break; + } + } + + return 0; // return success +} + +/** + * @brief Converts the given time as sentence (String) + * + * @param hours hours of the time value + * @param minutes minutes of the time value + * @return String time as sentence + */ +String time_to_string(uint8_t hours, uint8_t minutes) +{ + String message = "ES IST "; // first two words + + // show minutes + if (minutes >= 5 && minutes < 10) + { + message += "FUNF NACH "; + } + else if (minutes >= 10 && minutes < 15) + { + message += "ZEHN NACH "; + } + else if (minutes >= 15 && minutes < 20) + { + message += "VIERTEL NACH "; + } + else if (minutes >= 20 && minutes < 25) + { + message += "ZEHN VOR HALB "; + } + else if (minutes >= 25 && minutes < 30) + { + message += "FUNF VOR HALB "; + } + else if (minutes >= 30 && minutes < 35) + { + message += "HALB "; + } + else if (minutes >= 35 && minutes < 40) + { + message += "FUNF NACH HALB "; + } + else if (minutes >= 40 && minutes < 45) + { + message += "ZEHN NACH HALB "; + } + else if (minutes >= 45 && minutes < 50) + { + message += "VIERTEL VOR "; + } + else if (minutes >= 50 && minutes < 55) + { + message += "ZEHN VOR "; + } + else if (minutes >= 55 && minutes < 60) + { + message += "FUNF VOR "; + } + + // convert hours to 12h format + if (hours >= 12) + { + hours -= 12; + } + if (minutes >= 20) + { + hours++; + } + if (hours == 12) + { + hours = 0; + } + + // show hours + switch (hours) + { + case 0: + message += "ZWOLF "; + break; + case 1: + message += "EIN"; + // EIN(S) + if (minutes > 4) + { + message += "S"; + } + message += " "; + break; + case 2: + message += "ZWEI "; + break; + case 3: + message += "DREI "; + break; + case 4: + message += "VIER "; + break; + case 5: + message += "FUNF "; + break; + case 6: + message += "SECHS "; + break; + case 7: + message += "SIEBEN "; + break; + case 8: + message += "ACHT "; + break; + case 9: + message += "NEUN "; + break; + case 10: + message += "ZEHN "; + break; + case 11: + message += "ELF "; + break; + } + if (minutes < 5) + { + message += "UHR "; + } + + return message; +} diff --git a/src/wrapper/base64_wrapper.cpp b/src/wrapper/base64_wrapper.cpp new file mode 100644 index 0000000..75a4be4 --- /dev/null +++ b/src/wrapper/base64_wrapper.cpp @@ -0,0 +1,143 @@ +/* +Copyright (C) 2016 Arturo Guadalupi. All right reserved. + +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. + +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. +*/ + +#include +#include "base64_wrapper.h" +#if (defined(__AVR__)) +#include +#else +#include +#endif + +const char PROGMEM _Base64AlphabetTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + +int Base64Class::encode(char *output, char *input, int inputLength) { + int i = 0, j = 0; + int encodedLength = 0; + unsigned char A3[3]; + unsigned char A4[4]; + + while(inputLength--) { + A3[i++] = *(input++); + if(i == 3) { + fromA3ToA4(A4, A3); + + for(i = 0; i < 4; i++) { + output[encodedLength++] = pgm_read_byte(&_Base64AlphabetTable[A4[i]]); + } + + i = 0; + } + } + + if(i) { + for(j = i; j < 3; j++) { + A3[j] = '\0'; + } + + fromA3ToA4(A4, A3); + + for(j = 0; j < i + 1; j++) { + output[encodedLength++] = pgm_read_byte(&_Base64AlphabetTable[A4[j]]); + } + + while((i++ < 3)) { + output[encodedLength++] = '='; + } + } + output[encodedLength] = '\0'; + return encodedLength; +} + +int Base64Class::decode(char * output, char * input, int inputLength) { + int i = 0, j = 0; + int decodedLength = 0; + unsigned char A3[3]; + unsigned char A4[4]; + + + while (inputLength--) { + if(*input == '=') { + break; + } + + A4[i++] = *(input++); + if (i == 4) { + for (i = 0; i <4; i++) { + A4[i] = lookupTable(A4[i]); + } + + fromA4ToA3(A3,A4); + + for (i = 0; i < 3; i++) { + output[decodedLength++] = A3[i]; + } + i = 0; + } + } + + if (i) { + for (j = i; j < 4; j++) { + A4[j] = '\0'; + } + + for (j = 0; j <4; j++) { + A4[j] = lookupTable(A4[j]); + } + + fromA4ToA3(A3,A4); + + for (j = 0; j < i - 1; j++) { + output[decodedLength++] = A3[j]; + } + } + output[decodedLength] = '\0'; + return decodedLength; +} + +int Base64Class::encodedLength(int plainLength) { + int n = plainLength; + return (n + 2 - ((n + 2) % 3)) / 3 * 4; +} + +int Base64Class::decodedLength(char * input, int inputLength) { + int i = 0; + int numEq = 0; + for(i = inputLength - 1; input[i] == '='; i--) { + numEq++; + } + + return ((6 * inputLength) / 8) - numEq; +} + +//Private utility functions +inline void Base64Class::fromA3ToA4(unsigned char * A4, unsigned char * A3) { + A4[0] = (A3[0] & 0xfc) >> 2; + A4[1] = ((A3[0] & 0x03) << 4) + ((A3[1] & 0xf0) >> 4); + A4[2] = ((A3[1] & 0x0f) << 2) + ((A3[2] & 0xc0) >> 6); + A4[3] = (A3[2] & 0x3f); +} + +inline void Base64Class::fromA4ToA3(unsigned char * A3, unsigned char * A4) { + A3[0] = (A4[0] << 2) + ((A4[1] & 0x30) >> 4); + A3[1] = ((A4[1] & 0xf) << 4) + ((A4[2] & 0x3c) >> 2); + A3[2] = ((A4[2] & 0x3) << 6) + A4[3]; +} + +inline unsigned char Base64Class::lookupTable(char c) { + if(c >='A' && c <='Z') return c - 'A'; + if(c >='a' && c <='z') return c - 71; + if(c >='0' && c <='9') return c + 4; + if(c == '+') return 62; + if(c == '/') return 63; + return -1; +} + +Base64Class Base64; diff --git a/src/wrapper/littlefs_wrapper.cpp b/src/wrapper/littlefs_wrapper.cpp new file mode 100644 index 0000000..9872ec3 --- /dev/null +++ b/src/wrapper/littlefs_wrapper.cpp @@ -0,0 +1,217 @@ + +// **************************************************************** +// Sketch Esp8266 Filesystem Manager spezifisch sortiert Modular(Tab) +// created: Jens Fleischer, 2020-06-08 +// last mod: Jens Fleischer, 2020-12-19 +// For more information visit: https://fipsok.de +// **************************************************************** +// Hardware: Esp8266 +// Software: Esp8266 Arduino Core 2.7.0 - 3.0.2 +// Geprueft: von 1MB bis 2MB Flash +// Getestet auf: Nodemcu +/****************************************************************** + Copyright (c) 2020 Jens Fleischer. All rights reserved. + + This file is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This file is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. +*******************************************************************/ +// Diese Version von LittleFS sollte als Tab eingebunden werden. +// #include #include muessen im Haupttab aufgerufen werden +// Die Funktionalitaet des ESP8266 Webservers ist erforderlich. +// "webserver.onNotFound()" darf nicht im Setup des ESP8266 Webserver stehen. +// Die Funktion "setup_filesystem();" muss im Setup aufgerufen werden. +/**************************************************************************************/ +#include +#include "littlefs_wrapper.h" +#include "LittleFS.h" +#include +#include +#include + +extern ESP8266WebServer webserver; + +const char WARNING[] PROGMEM = R"(

Der Sketch wurde mit "FS:none" kompiliert!)"; +const char HELPER[] PROGMEM = R"(
+
Lade die fs.html hoch.)"; + +void setup_filesystem() +{ // Funktionsaufruf "setup_filesystem();" muss im Setup eingebunden werden + LittleFS.begin(); + webserver.on("/format", format_filesystem); + webserver.on("/upload", HTTP_POST, send_response, handle_upload); + webserver.onNotFound([]() + { + if (!handle_file(webserver.urlDecode(webserver.uri()))) + webserver.send(404, "text/plain", "FileNotFound"); }); +} + +bool handle_list() +{ // Senden aller Daten an den Client + FSInfo fs_info; + LittleFS.info(fs_info); // Fuellt FSInfo Struktur mit Informationen ueber das Dateisystem + Dir dir = LittleFS.openDir("/"); + using namespace std; + using records = tuple; + list dirList; + while (dir.next()) // Ordner und Dateien zur Liste hinzufuegen + { + if (dir.isDirectory()) + { + uint8_t ran{0}; + Dir fold = LittleFS.openDir(dir.fileName()); + while (fold.next()) + { + ran++; + dirList.emplace_back(dir.fileName(), fold.fileName(), fold.fileSize()); + } + if (!ran) + dirList.emplace_back(dir.fileName(), "", 0); + } + else + { + dirList.emplace_back("", dir.fileName(), dir.fileSize()); + } + } + dirList.sort([](const records &f, const records &l) { // Dateien sortieren + if (webserver.arg(0) == "1") + { + return get<2>(f) > get<2>(l); + } + else + { + for (uint8_t i = 0; i < 31; i++) + { + if (tolower(get<1>(f)[i]) < tolower(get<1>(l)[i])) + return true; + else if (tolower(get<1>(f)[i]) > tolower(get<1>(l)[i])) + return false; + } + return false; + } + }); + dirList.sort([](const records &f, const records &l) { // Ordner sortieren + if (get<0>(f)[0] != 0x00 || get<0>(l)[0] != 0x00) + { + for (uint8_t i = 0; i < 31; i++) + { + if (tolower(get<0>(f)[i]) < tolower(get<0>(l)[i])) + return true; + else if (tolower(get<0>(f)[i]) > tolower(get<0>(l)[i])) + return false; + } + } + return false; + }); + String temp = "["; + for (auto &t : dirList) + { + if (temp != "[") + temp += ','; + temp += "{\"folder\":\"" + get<0>(t) + "\",\"name\":\"" + get<1>(t) + "\",\"size\":\"" + format_bytes(get<2>(t)) + "\"}"; + } + temp += ",{\"usedBytes\":\"" + format_bytes(fs_info.usedBytes) + // Berechnet den verwendeten Speicherplatz + "\",\"totalBytes\":\"" + format_bytes(fs_info.totalBytes) + // Zeigt die Groeße des Speichers + "\",\"freeBytes\":\"" + (fs_info.totalBytes - fs_info.usedBytes) + "\"}]"; // Berechnet den freien Speicherplatz + webserver.send(200, "application/json", temp); + return true; +} + +void delete_recursive(const String &path) +{ + if (LittleFS.remove(path)) + { + LittleFS.open(path.substring(0, path.lastIndexOf('/')) + "/", "w"); + return; + } + Dir dir = LittleFS.openDir(path); + while (dir.next()) + { + delete_recursive(path + '/' + dir.fileName()); + } + LittleFS.rmdir(path); +} + +bool handle_file(String &&path) +{ + if (webserver.hasArg("new")) + { + String folderName{webserver.arg("new")}; + for (auto &c : {34, 37, 38, 47, 58, 59, 92}) + for (auto &e : folderName) + if (e == c) + { + e = 95; // Ersetzen der nicht erlaubten Zeichen + } + LittleFS.mkdir(folderName); + } + if (webserver.hasArg("sort")) + { + return handle_list(); + } + if (webserver.hasArg("delete")) + { + delete_recursive(webserver.arg("delete")); + send_response(); + return true; + } + if (!LittleFS.exists("fs.html")) + webserver.send(200, "text/html", LittleFS.begin() ? HELPER : WARNING); // ermoeglicht das hochladen der fs.html + if (path.endsWith("/")) + { + path += "index.html"; + } + if (path == "/spiffs.html") + { + send_response(); // Vorruebergehend fuer den Admin Tab + } + return LittleFS.exists(path) ? ({File f = LittleFS.open(path, "r"); webserver.streamFile(f, mime::getContentType(path)); f.close(); true; }) : false; +} + +void handle_upload() // Dateien ins Filesystem schreiben +{ + static File fsUploadFile; + HTTPUpload &upload = webserver.upload(); + if (upload.status == UPLOAD_FILE_START) + { + if (upload.filename.length() > 31) + { // Dateinamen kuerzen + upload.filename = upload.filename.substring(upload.filename.length() - 31, upload.filename.length()); + } + printf(PSTR("handleFileUpload Name: /%s\n"), upload.filename.c_str()); + fsUploadFile = LittleFS.open(webserver.arg(0) + "/" + webserver.urlDecode(upload.filename), "w"); + } + else if (upload.status == UPLOAD_FILE_WRITE) + { + printf(PSTR("handleFileUpload Data: %u\n"), upload.currentSize); + fsUploadFile.write(upload.buf, upload.currentSize); + } + else if (upload.status == UPLOAD_FILE_END) + { + printf(PSTR("handleFileUpload Size: %u\n"), upload.totalSize); + fsUploadFile.close(); + } +} + +void format_filesystem() // Formatiert das Filesystem +{ + LittleFS.format(); + send_response(); +} + +void send_response() +{ + webserver.sendHeader("Location", "fs.html"); + webserver.send(303, "message/http"); +} + +const String format_bytes(size_t const &bytes) +{ // lesbare Anzeige der Speichergroeßen + return bytes < 1024 ? static_cast(bytes) + " Byte" : bytes < 1048576 ? static_cast(bytes / 1024.0) + " KB" + : static_cast(bytes / 1048576.0) + " MB"; +} diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html