Compare commits

...

23 Commits

Author SHA1 Message Date
1629f24fcc Merge branch 'main' of ssh://192.168.178.10:2022/ranse/wordclock 2024-10-05 17:14:39 +02:00
adb3677d71 Bump FastLED to latest ESP8266 stable version 3.7.6. Bump WiFiManager to 2.0.17. 2024-10-05 17:13:57 +02:00
d6f06525c9 Update README.md. 2024-07-21 20:04:50 +02:00
4d810783fc Add diagnosis module via HTTP. 2024-07-21 19:30:21 +02:00
feb26d7269 Remove array initialization with single 0 because of compiler compatibility (espressif8266@2.6.3). 2024-07-21 19:30:06 +02:00
3dfd6c82fe Remove serial output in UDP Logger. 2024-07-21 19:28:38 +02:00
244455909d Use FastLED instead of NeoMatrix. Downgrade platform platform "espressif8266" to version 2.6.3 because of FastLED. 2024-07-21 19:27:58 +02:00
f53f557a6a Use FastLED instead of NeoMatrix. 2024-07-21 19:26:56 +02:00
e1df24633a Add diagnosis script and refactor UDP receiver. 2024-07-21 19:24:20 +02:00
4543cf1e09 Add backup and Python files. 2024-07-21 19:22:29 +02:00
0cf2e5ca90 Minor refactoring. 2024-05-28 19:07:00 +02:00
ff90c610ae Rework TimeManager & NTP handling. Remove module ESP8266TimerInterrupt. 2024-05-28 19:06:39 +02:00
2e50dd0e3f Remove module ESP8266TimerInterrupt. 2024-05-28 19:04:50 +02:00
b584cc03bd Minor refactoring. 2024-04-07 01:14:17 +02:00
eb4341e05b Fix for NTP update time. Minor refactoring. 2024-04-04 21:37:57 +02:00
73b390d8cd Introduction of state machine for TimeManager. Minor refactoring. 2024-04-04 16:40:16 +02:00
5a51707452 Add NTP update state. Refactoring. 2024-04-04 01:45:11 +02:00
f4b49dd8c4 Removed additional stdout StreamHandler. 2024-04-03 21:49:47 +02:00
a4be8f1d9e Introduced new TimeManager class with much better offline handling. Refactoring. 2024-04-03 21:49:28 +02:00
0543b9c0c7 Replace base64 files with library. Major refactoring. 2024-04-03 01:10:15 +02:00
ef6061fc21 Fix of NTP logic. Minor refactoring. 2024-04-02 03:43:42 +02:00
73aa168152 Removal of ntp_client_plus files, refactoring to use time C lib instead. 2024-04-01 03:32:54 +02:00
52c7794d59 Add explicit Adafruit BusIO dependency. 2024-04-01 03:28:07 +02:00
25 changed files with 842 additions and 1326 deletions

13
.gitignore vendored
View File

@@ -65,4 +65,15 @@ log.txt
modules.order
Module.symvers
Mkfile.old
dkms.conf
dkms.conf
# Python
venv
.venv
# Logs
*.log
# Local backup
*.*_
*.*bak*

View File

@@ -1,96 +1,49 @@
# Important Note:
This project has been unofficially forked from https://github.com/techniccontroller/wordclock_esp8266 which was initially created by techniccontroller. Copyright and licensing is respected.
This project has been unofficially forked from https://github.com/techniccontroller/wordclock_esp8266 which was initially created by techniccontroller. Copyright and licensing is respected. Very many thanks for the initial code!
# Wordclock 2.0
Wordclock 2.0 with ESP8266 and NTP time
More details on my website: https://techniccontroller.com/word-clock-with-wifi-and-neopixel/
More details on techniccontroller's 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.
The Wordclock is available in **German** language.
## 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
- Time update via NTP server
- Games: Pong, Snake, Tetris, ...
- Automatic summer/wintertime change
- Easy wifi setup with WifiManager
- Configurable color
- Configurable night mode (start and end time)
- Configurable brightness
- Automatic mode change
- Webserver interface for configuration and control (web address: wordclock.local)
- Automatic current limiting of LEDs
## Pictures of clock
![modes_images2](https://user-images.githubusercontent.com/36072504/156947689-dd90874d-a887-4254-bede-4947152d85c1.png)
## 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.
<img src="https://techniccontroller.com/wp-content/uploads/filemanager1-1.png" height="300px" /> <img src="https://techniccontroller.com/wp-content/uploads/filemanager2-1.png" height="300px" /> <img src="https://techniccontroller.com/wp-content/uploads/filemanager3-1.png" height="300px" />
## 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
1. Clone the project into the a directory.
2. Open directory in VSCode with PlatformIO extension installed.
3. Current dependencies (2024-07-21):
- Platform espressif8266 @ 2.6.3 (required: espressif8266 @ 2.6.3)
- Libraries
- ├── FastLED @ 3.7.0 (required: fastled/FastLED @ 3.7.0)
- ├── FastLED NeoMatrix @ 1.2.0 (required: marcmerlin/FastLED NeoMatrix @ ^1.2)
- │⠀⠀⠀└── Framebuffer GFX @ 1.1.0 (required: Framebuffer GFX)
- │⠀⠀⠀│⠀⠀⠀├── Adafruit GFX Library @ 1.11.9 (required: Adafruit GFX Library)
- │⠀⠀⠀│⠀⠀⠀│⠀⠀⠀└── Adafruit BusIO @ 1.16.1 (required: Adafruit BusIO)
- ├── WiFiManager @ 0.16.0 (required: tzapu/WiFiManager @ ^0.16.0)
- └── base64 @ 1.4.0 (required: densaugeo/base64 @ ^1.4.0)
## 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.
By default the WifiManager is activated. That is, the word clock makes the first time its own WiFi (should be called "WordclockAP"). There you simply connect to the cell phone and you can perform configuration of the WiFi settings conveniently as with SmartHome devices.

View File

@@ -4,6 +4,8 @@
#include <Arduino.h>
#include "wordclock_constants.h"
extern bool spiral_direction; // Direction of sprial animation
enum Direction
{
RIGHT,

View File

@@ -1,26 +0,0 @@
/*
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 */

26
include/diagnosis.h Normal file
View File

@@ -0,0 +1,26 @@
#ifndef DIAGNOSIS_H
#define DIAGNOSIS_H
#include <Arduino.h>
#include "led_matrix.h"
#include "udp_logger.h"
class Diagnosis
{
public:
Diagnosis(); // constructor
Diagnosis(UDPLogger *logger, LEDMatrix *matrix); // constructor
String handle_command(const String &command);
String print_device_info();
String print_sketch_info();
String print_last_reset_details();
String print_matrix_fps();
private:
UDPLogger *_logger;
LEDMatrix * _matrix;
void print(const String &s);
};
#endif // DIAGNOSIS_H

View File

@@ -1,24 +1,28 @@
#ifndef LEDMATRIX_H
#define LEDMATRIX_H
#ifndef FASTLED_INTERNAL
#define FASTLED_INTERNAL
#endif
#include <Arduino.h>
#include <Adafruit_GFX.h>
#include <Adafruit_NeoMatrix.h>
#include <FastLED_NeoMatrix.h>
#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);
LEDMatrix(FastLED_NeoMatrix *matrix, uint8_t brightness, UDPLogger *logger);
static uint16_t color_24_to_16bit(uint32_t color24bit);
static uint32_t color_24bit(uint8_t r, uint8_t g, uint8_t b);
static uint32_t interpolate_color_24bit(uint32_t color1, uint32_t color2, float factor);
static uint32_t wheel(uint8_t WheelPos);
uint16_t get_fps(void);
void draw_on_matrix_instant();
void draw_on_matrix_smooth(float factor);
void flush(void);
@@ -31,26 +35,26 @@ public:
void setup_matrix();
private:
Adafruit_NeoMatrix * _neomatrix;
UDPLogger * _logger;
FastLED_NeoMatrix *_neomatrix;
UDPLogger *_logger;
uint8_t _brightness;
uint16_t _current_limit;
// target representation of matrix as 2D array
uint32_t _target_grid[MATRIX_HEIGHT][MATRIX_WIDTH] = {0};
uint32_t _target_grid[MATRIX_HEIGHT][MATRIX_WIDTH];
// current representation of matrix as 2D array
uint32_t _current_grid[MATRIX_HEIGHT][MATRIX_WIDTH] = {0};
uint32_t _current_grid[MATRIX_HEIGHT][MATRIX_WIDTH];
// target representation of minutes indicator leds
// target representation of minutes indicator LEDs
uint32_t _target_minute_indicators[4] = {0, 0, 0, 0};
// current representation of minutes indicator leds
// current representation of minutes indicator LEDs
uint32_t _current_minute_indicators[4] = {0, 0, 0, 0};
void _draw_on_matrix(float factor);
uint16_t _calc_estimated_led_current(uint32_t color);
};
#endif /* LEDMATRIX_H */
#endif /* LEDMATRIX_H */

View File

@@ -1,90 +0,0 @@
#ifndef NTPCLIENTPLUS_H
#define NTPCLIENTPLUS_H
#include <Arduino.h>
#include <WiFiUdp.h>
#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
} NtpReturnValue;
/**
* @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 *pool_server_name, int utcx, bool sw_change);
bool is_leap_year(unsigned int year);
bool check_daylight_saving_time();
int get_hours_12() const;
int get_hours_24() const;
int get_minutes() const;
int get_month(int dayOfYear);
int get_seconds() const;
int update_ntp();
long get_time_offset();
String get_formatted_date();
String get_formatted_time() const;
unsigned int get_day_of_week();
unsigned int get_year();
unsigned long get_epoch_time() const;
unsigned long get_secs_since_1900() const;
void calc_date();
void end();
void set_pool_server_name(const char *pool_server_name);
void set_time_offset(int time_offset);
void setup_ntp_client();
private:
UDP *_udp;
bool _udp_setup = false;
bool _sw_change = 1;
const char *_pool_server_name = "pool.ntp.org"; // Default time server
int _utcx = 0;
IPAddress _pool_server_ip;
long _time_offset = 0;
unsigned int _port = NTP_DEFAULT_LOCAL_PORT;
unsigned long _update_interval = 60000; // In ms
unsigned int _date_day = 0;
unsigned int _date_month = 0;
unsigned int _date_year = 0;
unsigned int _day_of_week = 0;
unsigned long _current_epoc = 0; // In s
unsigned long _last_secs_since_1900 = 0;
unsigned long _last_update = 0; // In ms
unsigned long _secs_since_1900 = 0; // seconds since 1. Januar 1900, 00:00:00
unsigned char _packet_buffer[NTP_PACKET_SIZE] = {0};
void send_ntp_packet();
void set_summertime(bool summertime);
static const unsigned long milliseconds_per_second = 1000;
static const unsigned long minutes_per_hour = 60;
static const unsigned long seconds_per_day = 86400;
static const unsigned long seconds_per_hour = 3600;
static const unsigned long seconds_per_minute = 60;
// number of days in months
unsigned int _days_per_month[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
};
void wait(unsigned long time_ms);
#endif /* NTPCLIENTPLUS_H */

View File

@@ -79,8 +79,8 @@ private:
UDPLogger *_logger;
uint8_t _gameState = 0;
uint8_t _numBots = 0;
uint8_t _playerMovement[PLAYER_AMOUNT] = {0};
Coords _paddles[PLAYER_AMOUNT][PADDLE_WIDTH] = {0};
uint8_t _playerMovement[PLAYER_AMOUNT];
Coords _paddles[PLAYER_AMOUNT][PADDLE_WIDTH];
Coords _ball = {0, 0};
Coords _ball_old = {0, 0};
int _ballMovement[2] = {0, 0};

View File

@@ -76,7 +76,7 @@ private:
uint8_t _userDirection = 0;
uint8_t _gameState = 0;
Coords _head = {0, 0};
Coords _tail[MAX_TAIL_LENGTH] = {0};
Coords _tail[MAX_TAIL_LENGTH];
Coords _food = {0, 0};
unsigned long _lastDrawUpdate = 0;
unsigned long _lastButtonClick = 0;

80
include/time_manager.h Normal file
View File

@@ -0,0 +1,80 @@
#ifndef TIME_MANAGER_H
#define TIME_MANAGER_H
#include <Arduino.h>
#include <time.h>
#include "udp_logger.h"
typedef enum
{
TIME_UPDATE_FAILED = 0,
TIME_UPDATE_OK = 1,
TIME_UPDATE_PENDING = 2,
} TimeUpdateState;
typedef enum
{
TM_INIT = 0,
TM_INITIAL_SYNC = 1,
TM_NORMAL = 2,
TM_SYNC_OVERDUE = 3,
TM_SYNC_TIMEOUT = 4,
TM_SETUP_FAILED = 5,
} TimeManagerState;
class TimeManager
{
#define NTP_MAX_UPDATE_TIME_US (5 * 1000 * 1000) // 5000ms max update time
public:
// constructors
TimeManager();
TimeManager(const char *tz,
const char *ntp_server,
bool (*is_wifi_connected)(void),
uint32 ntp_max_offline_time_s,
UDPLogger *logger);
// init
void init();
// callback
void time_set_cb(void); // callback which is called when NTP time was set
// ntp methods
bool ntp_sync_successful(void) const; // was there a NTP sync once?
bool ntp_sync_overdue(void); // function to check if NTP sync is overdue
bool ntp_sync_timeout(void); // function to check if maximum time since last NTP sync has been reached
TimeUpdateState get_time(); // main time update method, called in loop
// getter for time values
bool isdst(void) const; // true if summertime (daylight saving time)
int day(void) const;
int hour(void) const;
int minute(void) const;
int month(void) const;
int year(void) const;
struct tm time_info(void) const;
// getter
TimeManagerState tm_state(void) const; // get current state
// logging
void log_time() const; // log _time_info
void log_time(struct tm time_info) const; // log argument time_info
private:
void _set_up_ntp(void); // set up NTP server
bool (*_is_wifi_connected)(void); // function to check if wifi is connected
const char *_ntp_server = "pool.ntp.org"; // ntp server address
const char *_tz; // timezone
struct tm _time_info = {0, 0, 0, 0, 0, 0, 0, 0, 0}; // structure tm holds time information
time_t _now = 0; // local time value
time_t _ntp_sync_timestamp_s = 0; // timestamp of last successful ntp sync
TimeManagerState _tm_state = TM_INIT; // Main state
UDPLogger *_logger; // logger instance
uint32 _ntp_max_offline_time_s; // maximum time in seconds which is considered ok since last NTP update
};
#endif /* TIME_MANAGER_H */

View File

@@ -6,15 +6,17 @@
// ----------------------------------------------------------------------------------
// CONSTANTS
// ----------------------------------------------------------------------------------
#define AP_SSID "WordclockAP" // SSID name of Access Point
#define NTP_SERVER_URL "de.pool.ntp.org" // NTP server address
#define AP_SSID "WordclockAP" // SSID name of Access Point
#define NTP_SERVER_URL "de.pool.ntp.org" // NTP server address
#define MY_TZ "CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00" // Timezone
#define HOSTNAME (String("wordclock")) // Local hostname
#define LOGGER_MULTICAST_IP (IPAddress(230, 120, 10, 2)) // IP for UDP server
#define LOGGER_MULTICAST_PORT (8123) // Port for UDP server
#define HTTP_PORT (80) // Standard HTTP port
// ESP8266 Pins
#define NEOPIXEL_PIN (14) // pin to which the NeoPixels are attached
#define FASTLED_PIN (14) // pin to which the LEDs are attached
#define BUTTON_PIN (5) // pin to which the button is attached
// Time limits
@@ -31,17 +33,18 @@
#define NIGHTMODE_END_MIN (0)
// Timings in us
#define PERIOD_ANIMATION_US (200 * 1000) // 200ms
#define PERIOD_CLOCK_UPDATE_US (1 * 1000 * 1000) // 1s
#define PERIOD_HEARTBEAT_US (1 * 1000 * 1000) // 1s
#define PERIOD_MATRIX_UPDATE_US (100 * 1000) // 100ms
#define PERIOD_NIGHTMODE_CHECK_US (20 * 1000 * 1000) // 20s
#define PERIOD_NTP_UPDATE_US (30 * 1000 * 1000) // 30s
#define PERIOD_PONG_US (10 * 1000) // 10ms
#define PERIOD_SNAKE_US (50 * 1000) // 50ms
#define PERIOD_STATE_CHANGE_US (10 * 1000 * 1000) // 10s
#define PERIOD_TETRIS_US (50 * 1000) // 50ms
#define TIMEOUT_LEDDIRECT_US (5 * 1000 * 1000) // 5s
#define PERIOD_ANIMATION_US (200 * 1000) // 200ms
#define PERIOD_CLOCK_UPDATE_US (1 * 1000 * 1000) // Must be 1s! Do not change!
#define PERIOD_HEARTBEAT_US (1 * 1000 * 1000) // 1s
#define PERIOD_MATRIX_UPDATE_US (33 * 1000) // 33ms
#define PERIOD_NIGHTMODE_CHECK_US (30 * 1000 * 1000) // 30s
#define PERIOD_TIME_UPDATE_US (1 * 1000 * 1000) // 1000ms
#define PERIOD_PONG_US (10 * 1000) // 10ms
#define PERIOD_SNAKE_US (50 * 1000) // 50ms
#define PERIOD_STATE_CHANGE_US (10 * 1000 * 1000) // 10s
#define PERIOD_TETRIS_US (50 * 1000) // 50ms
#define TIMEOUT_LEDDIRECT_US (5 * 1000 * 1000) // 5s
#define PERIOD_BRIGHTNESS_UPDATE_US (5 * 60 * 1000 * 1000) // 300s
#define SHORT_PRESS_US (100 * 1000) // 100ms
#define LONG_PRESS_US (2 * 1000 * 1000) // 2s
@@ -60,9 +63,17 @@
// Number of colors in colors array
#define NUM_COLORS (7)
#define COLOR_ORDER GRB // WS2812B color order
// LED matrix size
#define MATRIX_WIDTH (11)
#define MATRIX_HEIGHT (11)
#define NUM_MATRIX (MATRIX_WIDTH * (MATRIX_HEIGHT + 1))
// NTP macros
#define BUILD_YEAR (__DATE__ + 7) // Will expand to current year at compile time as string.
#define NTP_MINIMUM_RX_YEAR (atoi(BUILD_YEAR) - 1) // Will expand to current year minus one at compile time.
#define NTP_START_YEAR (1900) // NTP minimum year is 1900
#define NTP_MAX_OFFLINE_TIME_S (7 * 24 * 3600) // Watchdog value, maximum offline time before a restart is triggered
#endif /* WORDCLOCK_CONSTANTS_H */

View File

@@ -2,6 +2,7 @@
#define WORDCLOCK_ESP8266_H
#include <Arduino.h>
#include <stdlib.h>
#include <ESP8266WebServer.h>
#include "led_matrix.h"
#include "udp_logger.h"
@@ -11,6 +12,9 @@
#define EEPROM_SIZE (sizeof(EepromLayout_st) / sizeof(uint8_t))
// ----------------------------------------------------------------------------------
// TYPEDEFS
// ----------------------------------------------------------------------------------
typedef struct
{
int start_hour;
@@ -24,7 +28,7 @@ typedef struct
uint8_t red;
uint8_t green;
uint8_t blue;
uint8_t alpha;
uint8_t alpha; // note: unused
} Color_st;
typedef struct
@@ -54,11 +58,15 @@ typedef enum
NUM_STATES
} ClockState_en;
// ----------------------------------------------------------------------------------
// FUNCTIONS DECLARATIONS
// ----------------------------------------------------------------------------------
bool check_wifi_status(void);
String leading_zero2digit(int value);
uint8_t calculate_dynamic_brightness(uint8_t min_brightness, uint8_t max_brightness, int hours, int minutes, bool summertime);
uint8_t update_brightness(void);
void check_night_mode(void);
void check_wifi_status(void);
void cold_start_setup(void);
void draw_main_color(void);
void handle_button(void);
void handle_command(void);
@@ -66,7 +74,7 @@ void handle_current_state(void);
void handle_data_request(void);
void handle_led_direct(void);
void limit_value_ranges(void);
void ntp_time_update(uint32 *last_ntp_update_us);
void log_data(void);
void on_state_entry(uint8_t state);
void read_settings_from_EEPROM(void);
void reset_wifi_credentials(void);
@@ -74,7 +82,7 @@ void send_heartbeat(void);
void set_dynamic_brightness(bool state);
void set_main_color(uint8_t red, uint8_t green, uint8_t blue);
void set_night_mode(bool on);
void state_change(uint8_t newState);
void state_change(ClockState_en new_state);
void update_matrix(void);
void write_settings_to_EEPROM(void);

View File

@@ -12,13 +12,16 @@
default_envs = nodemcuv2
[env]
platform = espressif8266
platform = espressif8266@2.6.3
board = nodemcuv2
framework = arduino
lib_deps =
adafruit/Adafruit NeoMatrix@^1.3.0
adafruit/Adafruit NeoPixel@^1.11.0
tzapu/WiFiManager@^0.16.0
densaugeo/base64@^1.4.0
fastled/FastLED@3.7.6
marcmerlin/FastLED NeoMatrix@^1.2
tzapu/WiFiManager@^2.0.17
build_flags =
-DFASTLED_ESP8266_RAW_PIN_ORDER
[env:nodemcuv2]
monitor_speed = 115200

13
scripts/http_diagnosis.py Normal file
View File

@@ -0,0 +1,13 @@
import requests
r = requests.get("http://wordclock.local/cmd?diag=reset_info")
print(r.status_code)
print(r.text)
r = requests.get("http://wordclock.local/cmd?diag=sketch_info")
print(r.status_code)
print(r.text)
r = requests.get("http://wordclock.local/cmd?diag=device_info")
print(r.status_code)
print(r.text)

View File

@@ -4,13 +4,15 @@ import queue
import socket
import struct
import sys
from pathlib import Path
LOG_PATH = Path("C:/temp/wordclock_log.txt")
def setup_logging():
FORMAT = "%(asctime)s %(message)s"
logging.basicConfig(format=FORMAT, level=logging.INFO)
logger = logging.getLogger()
handler = logging.StreamHandler(sys.stdout)
handler = logging.FileHandler(LOG_PATH)
handler.setLevel(logging.INFO)
logger.addHandler(handler)
return logger
@@ -42,33 +44,9 @@ mreq = struct.pack("4s4s", group, socket.inet_aton(get_ip_address()))
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
logger.info("Ready")
saveCounter = 0
buffer = queue.Queue(20)
# Receive/respond loop
while True:
data, address = sock.recvfrom(1024)
data_str = data.decode("utf-8").strip()
logger.info(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

BIN
scripts/requirements.txt Normal file

Binary file not shown.

View File

@@ -0,0 +1,93 @@
#include "diagnosis.h"
Diagnosis::Diagnosis()
{
_logger = nullptr;
}
Diagnosis::Diagnosis(UDPLogger *logger, LEDMatrix *matrix) // constructor
{
_logger = logger;
_matrix = matrix;
}
String Diagnosis::handle_command(const String &command)
{
if (command == "device_info")
{
return print_device_info();
}
else if (command == "sketch_info")
{
return print_sketch_info();
}
else if (command == "reset_info")
{
return print_last_reset_details();
}
else if (command == "matrix_fps")
{
return print_matrix_fps();
}
else
{
// Handle unknown command
String unknown_command = "Diagnosis: Unknown command!\n";
print(unknown_command);
return unknown_command;
}
}
String Diagnosis::print_device_info()
{
// Retrieve and print device information
String device_info = "Device Information:\n";
device_info += "Chip ID: " + String(ESP.getChipId()) + "\n";
device_info += "Flash Chip ID: " + String(ESP.getFlashChipId()) + "\n";
device_info += "Flash Chip Size: " + String(ESP.getFlashChipSize()) + " bytes\n";
device_info += "Free Heap Size: " + String(ESP.getFreeHeap()) + " bytes\n";
device_info += "Free Sketch Space: " + String(ESP.getFreeSketchSpace()) + " bytes\n";
device_info += "SDK Version: " + String(ESP.getSdkVersion()) + "\n";
print(device_info);
return device_info;
}
String Diagnosis::print_sketch_info()
{
// Retrieve and print sketch information
String sketch_info = "Sketch Information:\n";
sketch_info += "Sketch Size: " + String(ESP.getSketchSize()) + " bytes\n";
sketch_info += "Sketch MD5: " + String(ESP.getSketchMD5()) + "\n";
print(sketch_info);
return sketch_info;
}
String Diagnosis::print_last_reset_details()
{
// Retrieve and print last reset details
String reset_info = "Last Reset Details:\n";
reset_info += "Reset Reason: " + String(ESP.getResetReason()) + "\n";
reset_info += "Reset Info: " + String(ESP.getResetInfo()) + "\n";
print(reset_info);
return reset_info;
}
String Diagnosis::print_matrix_fps()
{
// Retrieve and print matrix FPS
String matrix_fps = "Matrix FPS: " + String(_matrix->get_fps()) + "\n";
print(matrix_fps);
return matrix_fps;
}
void Diagnosis::print(const String &s)
{
if (_logger != nullptr)
{
_logger->log_string(s);
}
else
{
Serial.println(s);
}
}

View File

@@ -1,658 +0,0 @@
#include <Arduino.h>
#include "ntp_client_plus.h"
/**
* @brief Construct a new NTPClientPlus::NTPClientPlus object
*
* @param udp UDP client
* @param pool_server_name time server name
* @param utcx UTC offset (in 1h)
* @param sw_change should summer/winter time be considered
*/
NTPClientPlus::NTPClientPlus(UDP &udp, const char *pool_server_name, int utcx, bool sw_change)
{
this->_udp = &udp;
this->_utcx = utcx;
this->_time_offset = this->seconds_per_hour * this->_utcx;
this->_pool_server_name = pool_server_name;
this->_sw_change = sw_change;
}
/**
* @brief Starts the underlying UDP client, get first NTP timestamp and calc date
*
*/
void NTPClientPlus::setup_ntp_client()
{
this->_udp->begin(this->_port);
this->_udp_setup = true;
this->update_ntp();
this->calc_date();
}
/**
* @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::update_ntp()
{
// flush any existing packets
while (this->_udp->parsePacket() != 0)
{
this->_udp->flush();
}
this->send_ntp_packet();
// 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->_packet_buffer, NTP_PACKET_SIZE);
unsigned long high_word = word(this->_packet_buffer[40], this->_packet_buffer[41]);
unsigned long low_word = word(this->_packet_buffer[42], this->_packet_buffer[43]);
// combine the four bytes (two words) into a long integer
// this is NTP time (seconds since Jan 1 1900):
unsigned long temp_secs_since_1900 = high_word << 16 | low_word;
if (temp_secs_since_1900 < 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->_last_secs_since_1900 == 0 || temp_secs_since_1900 - this->_last_secs_since_1900 < 100000)
{
// Only update time then account for delay in reading the time
this->_last_update = (system_get_time() / 1000) - (NTP_RECEIVE_WAIT_TIME_MS * (conn_tries + 1));
this->_secs_since_1900 = temp_secs_since_1900;
this->_current_epoc = this->_secs_since_1900 - UNIX_TIMESTAMP_1900;
// Remember time of last update
this->_last_secs_since_1900 = temp_secs_since_1900;
return NTP_UPDATE_SUCCESS; // return 0 after successful update
}
else
{
// Remember time of last update
this->_last_secs_since_1900 = temp_secs_since_1900;
return NTP_UPDATE_DIFFTOOHIGH;
}
}
/**
* @brief Stops the underlying UDP client
*
*/
void NTPClientPlus::end()
{
this->_udp->stop();
this->_udp_setup = false;
}
/**
* @brief Setter TimeOffset
*
* @param time_offset offset from UTC in seconds
*/
void NTPClientPlus::set_time_offset(int time_offset)
{
this->_time_offset = time_offset;
}
long NTPClientPlus::get_time_offset()
{
return this->_time_offset;
}
/**
* @brief Set time server name
*
* @param pool_server_name
*/
void NTPClientPlus::set_pool_server_name(const char *pool_server_name)
{
this->_pool_server_name = pool_server_name;
}
/**
* @brief Calc seconds since 1. Jan. 1900
*
* @return unsigned long seconds since 1. Jan. 1900
*/
unsigned long NTPClientPlus::get_secs_since_1900() const
{
return this->_time_offset + // User offset
this->_secs_since_1900 + // seconds returned by the NTP server
(((system_get_time() / 1000) - this->_last_update) / 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::get_epoch_time() const
{
return this->get_secs_since_1900() - UNIX_TIMESTAMP_1900;
}
/**
* @brief Get current hours in 24h format
*
* @return int
*/
int NTPClientPlus::get_hours_24() const
{
int hours = ((this->get_epoch_time() % 86400L) / 3600);
return hours;
}
/**
* @brief Get current hours in 12h format
*
* @return int
*/
int NTPClientPlus::get_hours_12() const
{
return this->get_hours_24() % 12;
}
/**
* @brief Get current minutes
*
* @return int
*/
int NTPClientPlus::get_minutes() const
{
return ((this->get_epoch_time() % 3600) / 60);
}
/**
* @brief Get current seconds
*
* @return int
*/
int NTPClientPlus::get_seconds() const
{
return this->get_epoch_time() % 60;
}
/**
* @brief
*
* @return String time formatted like `hh:mm:ss`
*/
String NTPClientPlus::get_formatted_time() const
{
unsigned long raw_time = this->get_epoch_time();
unsigned long hours = (raw_time % 86400L) / 3600;
String hours_str = hours < 10 ? "0" + String(hours) : String(hours);
unsigned long minutes = (raw_time % 3600) / 60;
String minute_str = minutes < 10 ? "0" + String(minutes) : String(minutes);
unsigned long seconds = raw_time % 60;
String second_str = seconds < 10 ? "0" + String(seconds) : String(seconds);
return hours_str + ":" + minute_str + ":" + second_str;
}
/**
* @brief
*
* @return String date formatted like `dd.mm.yyyy`
*/
String NTPClientPlus::get_formatted_date()
{
this->calc_date();
unsigned int dateDay = this->_date_day;
unsigned int dateMonth = this->_date_month;
unsigned int dateYear = this->_date_year;
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::calc_date()
{
// get days since 1900
unsigned long days1900 = this->get_secs_since_1900() / seconds_per_day;
// calc current year
this->_date_year = this->get_year();
// calc how many leap days since 1.Jan 1900
int leap_days = 0;
for (unsigned int i = 1900; i < this->_date_year; i++)
{
// check if leap year
if (this->is_leap_year(i))
{
leap_days++;
}
}
leap_days = leap_days - 1;
// check if current year is leap year
if (this->is_leap_year(this->_date_year))
{
_days_per_month[2] = 29;
}
else
{
_days_per_month[2] = 28;
}
unsigned int day_of_year = (days1900 - ((this->_date_year - 1900) * 365) - leap_days);
// calc current month
this->_date_month = this->get_month(day_of_year);
this->_date_day = 0;
// calc day of month
for (unsigned int i = 0; i < this->_date_month; i++)
{
this->_date_day = this->_date_day + _days_per_month[i];
}
this->_date_day = day_of_year - this->_date_day;
// 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->_day_of_week = 1;
for (unsigned int i = 0; i < days1900; i++)
{
if (this->_day_of_week < 7)
{
this->_day_of_week = this->_day_of_week + 1;
}
else
{
this->_day_of_week = 1;
}
}
// End: Calc date (dateDay, dateMonth, dateYear)
// calc if summer time active
this->check_daylight_saving_time();
}
/**
* @brief Getter for day of the week
*
* @return unsigned int
*/
unsigned int NTPClientPlus::get_day_of_week()
{
return this->_day_of_week;
}
/**
* @brief Function to calc current year
*
* @return unsigned int
*/
unsigned int NTPClientPlus::get_year()
{
unsigned long secs_since_1900 = this->get_secs_since_1900();
// NTP starts at 1. Jan 1900
unsigned int result = 1900;
unsigned int days_in_year = 0;
unsigned int days = 0;
unsigned int days_since_1900 = 0;
unsigned int for_i = 0;
bool leap_year = false;
days_since_1900 = secs_since_1900 / this->seconds_per_day;
for (for_i = 0; for_i < days_since_1900; for_i++)
{
leap_year = this->is_leap_year(result);
if (leap_year)
{
days_in_year = 366;
}
else
{
days_in_year = 365;
}
days++;
if (days >= days_in_year)
{
result++;
days = 0;
}
}
return result;
}
/**
* @brief Function to check if given year is leap year
*
* @param year
* @return true
* @return false
*/
bool NTPClientPlus::is_leap_year(unsigned int year)
{
bool result = false;
// check for leap year
if ((year % 4) == 0)
{
result = true;
if ((year % 100) == 0)
{
result = false;
if ((year % 400) == 0)
{
result = true;
}
}
}
else
{
result = false;
}
return result;
}
/**
* @brief Get Month of given day of year
*
* @param day_of_year
* @return int
*/
int NTPClientPlus::get_month(int day_of_year)
{
bool leap_year = this->is_leap_year(this->get_year());
// Month beginnings
int month_min[13] = {0, 1, 32, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335};
// Month endings
int month_max[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 (leap_year == true)
{
for (y = 3; y < 13; y++)
{
++month_min[y];
}
for (y = 2; y < 13; y++)
{
++month_max[y];
}
}
// January
if (day_of_year >= month_min[1] && day_of_year <= month_max[1])
{
month = 1;
}
// February
if (day_of_year >= month_min[2] && day_of_year <= month_max[2])
{
month = 2;
}
// March
if (day_of_year >= month_min[3] && day_of_year <= month_max[3])
{
month = 3;
}
// April
if (day_of_year >= month_min[4] && day_of_year <= month_max[4])
{
month = 4;
}
// May
if (day_of_year >= month_min[5] && day_of_year <= month_max[5])
{
month = 5;
}
// June
if (day_of_year >= month_min[6] && day_of_year <= month_max[6])
{
month = 6;
}
// July
if (day_of_year >= month_min[7] && day_of_year <= month_max[7])
{
month = 7;
}
// August
if (day_of_year >= month_min[8] && day_of_year <= month_max[8])
{
month = 8;
}
// September
if (day_of_year >= month_min[9] && day_of_year <= month_max[9])
{
month = 9;
}
// October
if (day_of_year >= month_min[10] && day_of_year <= month_max[10])
{
month = 10;
}
// November
if (day_of_year >= month_min[11] && day_of_year <= month_max[11])
{
month = 11;
}
// December
if (day_of_year >= month_min[12] && day_of_year <= month_max[12])
{
month = 12;
}
return month;
}
/**
* @brief (private) Send NTP Packet to NTP server
*
*/
void NTPClientPlus::send_ntp_packet()
{
// set all bytes in the buffer to 0
memset(this->_packet_buffer, 0, NTP_PACKET_SIZE);
// Initialize values needed to form NTP request
this->_packet_buffer[0] = 0b11100011; // LI, Version, Mode
this->_packet_buffer[1] = 0; // Stratum, or type of clock
this->_packet_buffer[2] = 6; // Polling Interval
this->_packet_buffer[3] = 0xEC; // Peer Clock Precision
// 8 bytes of zero for Root Delay & Root Dispersion
this->_packet_buffer[12] = 49;
this->_packet_buffer[13] = 0x4E;
this->_packet_buffer[14] = 49;
this->_packet_buffer[15] = 52;
// all NTP fields have been given values, now
// you can send a packet requesting a timestamp:
if (this->_pool_server_name)
{
this->_udp->beginPacket(this->_pool_server_name, 123);
}
else
{
this->_udp->beginPacket(this->_pool_server_ip, 123);
}
this->_udp->write(this->_packet_buffer, NTP_PACKET_SIZE);
this->_udp->endPacket();
}
/**
* @brief (private) Set time offset accordance to summer time
*
* @param summertime
*/
void NTPClientPlus::set_summertime(bool summertime)
{
if (summertime)
{
this->_time_offset = this->seconds_per_hour * (this->_utcx + 1);
}
else
{
this->_time_offset = this->seconds_per_hour * (this->_utcx);
}
}
/**
* @brief (private) Update Summer/Winter time change
*
* @returns bool summertime active
*/
bool NTPClientPlus::check_daylight_saving_time()
{
unsigned int day_of_week = this->_day_of_week;
unsigned int date_day = this->_date_day;
unsigned int date_month = this->_date_month;
bool summertime_active = false;
if (this->_sw_change)
{
// Start: Set summer-/ winter time
// current month is march
if (date_month == 3)
{
// it is last week in march
if ((this->_days_per_month[3] - date_day) < 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 folfalses 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 folfalses within the last week in March one more Sunday => set winter time
if (((this->_days_per_month[3] - date_day) + day_of_week) >= 7)
{
this->set_summertime(0);
summertime_active = false;
}
else // last sunday in march already over -> summer time
{
this->set_summertime(1);
summertime_active = true;
}
}
else // restart in first three weeks of march -> winter time
{
this->set_summertime(0);
summertime_active = false;
}
}
// current month is october
else if (date_month == 10)
{
// restart last week of october
if ((this->_days_per_month[10] - date_day) < 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 folfalses another Sunday in October => set summer time
// There folfalses within the last week in October one more Sunday => summer time
if (((this->_days_per_month[10] - date_day) + day_of_week) >= 7)
{
this->set_summertime(1);
summertime_active = true;
}
else // last sunday in october already over -> winter time
{
this->set_summertime(0);
summertime_active = false;
}
}
else // restart in first three weeks of october -> summer time
{
this->set_summertime(1);
summertime_active = true;
}
}
else if (date_month > 3 && date_month < 10) // restart in summer time
{
this->set_summertime(1);
summertime_active = true;
}
else if (date_month < 3 || date_month > 10) // restart in winter time
{
this->set_summertime(0);
summertime_active = false;
}
}
return summertime_active;
}
void wait(unsigned long time_ms)
{
unsigned long start = (system_get_time() / 1000); // in ms
while (((system_get_time() / 1000) - start) < time_ms)
{
yield();
};
}

View File

@@ -0,0 +1,214 @@
#include "time_manager.h"
#include "wordclock_constants.h"
#include <coredecls.h> // required for settimeofday_cb()
#include <sntp.h>
#include <time.h>
extern UDPLogger logger; // logging instance
// ----------------------------------------------------------------------------------
// Class
// ----------------------------------------------------------------------------------
TimeManager::TimeManager(){};
TimeManager::TimeManager(const char *tz,
const char *ntp_server,
bool (*is_wifi_connected)(void),
uint32 ntp_max_offline_time_s,
UDPLogger *logger)
{
_tz = tz;
_ntp_server = ntp_server;
_is_wifi_connected = is_wifi_connected;
_ntp_max_offline_time_s = ntp_max_offline_time_s;
_logger = logger;
}
void TimeManager::init()
{
// Set up NTP server once
_set_up_ntp();
if (_tm_state == TM_INITIAL_SYNC)
{
// force sntp reinit now
sntp_stop();
sntp_init();
}
settimeofday_cb([&]() { time_set_cb(); });
}
bool TimeManager::ntp_sync_successful(void) const
{
return (_now > 1716913300); // UTC timestamp in the past (28.05.2024)
}
bool TimeManager::ntp_sync_overdue(void)
{
if (!ntp_sync_successful())
{
return false;
}
bool retval = false;
// after the ntp sync update delay has been reached six times, the sync is considered overdue
if (_now >= (_ntp_sync_timestamp_s + (time_t)(6 * (sntp_update_delay_MS_rfc_not_less_than_15000() / 1000))))
{
_tm_state = TM_SYNC_OVERDUE;
retval = true;
}
return retval;
}
bool TimeManager::ntp_sync_timeout(void)
{
if (!ntp_sync_successful())
{
return false;
}
bool retval = false;
// after the maxmimum offline time has been reached, the sync is considered timed out
if (_now >= (_ntp_sync_timestamp_s + (time_t)_ntp_max_offline_time_s))
{
_tm_state = TM_SYNC_TIMEOUT;
retval = true;
}
return retval;
}
/**
* @brief NTP time update, should be called in loop().
*
* @retval true if last update was successful
*/
TimeUpdateState TimeManager::get_time()
{
TimeUpdateState retval = TIME_UPDATE_PENDING; // NTP time update
uint32 timestamp_us = system_get_time(); // NTP update start time
struct tm time_info; // local NTP time info
do
{
_now = time(nullptr); // update time
(void)localtime_r(&_now, &time_info); // convert time
yield(); // since this loop could take up to NTP_MAX_UPDATE_TIME_US
} while (((system_get_time() - timestamp_us) <= NTP_MAX_UPDATE_TIME_US) && (time_info.tm_year < (NTP_MINIMUM_RX_YEAR - NTP_START_YEAR)));
retval = (time_info.tm_year <= (NTP_MINIMUM_RX_YEAR - NTP_START_YEAR)) ? TIME_UPDATE_FAILED : TIME_UPDATE_OK; // sanity check
if (retval == TIME_UPDATE_OK)
{
_time_info = time_info; // take over time_info to member variable
_tm_state = TM_NORMAL;
}
else
{
logger.log_string("NTP-Update was not successful. Retrying in " + String(sntp_update_delay_MS_rfc_not_less_than_15000()) + "ms.");
}
return retval;
}
bool TimeManager::isdst(void) const
{
return (_time_info.tm_isdst > 0);
}
int TimeManager::day(void) const
{
return (_time_info.tm_mday + 1); // add 1 to get actual day
}
int TimeManager::hour(void) const
{
return _time_info.tm_hour;
}
int TimeManager::minute(void) const
{
return _time_info.tm_min;
}
int TimeManager::month(void) const
{
return (_time_info.tm_mon + 1); // add 1 to get actual month
}
int TimeManager::year(void) const
{
return _time_info.tm_year + NTP_START_YEAR; // add start year to get actual year
}
struct tm TimeManager::time_info(void) const
{
return _time_info;
}
TimeManagerState TimeManager::tm_state(void) const
{
return _tm_state;
}
/**
* @brief Log time_info as string.
*
* @param local_time
*/
void TimeManager::log_time() const
{
log_time(_time_info);
}
/**
* @brief Log time_info as string.
*
* @param local_time
*/
void TimeManager::log_time(struct tm time_info) const
{
char strftime_buf[64]; // Time string buffer
strftime(strftime_buf, sizeof(strftime_buf), "%c", &time_info);
logger.log_string(String(strftime_buf));
}
/**
* @brief Sets up the NTP server config
*
*/
void TimeManager::_set_up_ntp(void)
{
if ((_tz != nullptr) && (_ntp_server != nullptr))
{
// set up NTP server and timezone at init
configTime(_tz, _ntp_server);
_tm_state = TM_INITIAL_SYNC;
logger.log_string(String("NTP configuration was set up successfully!"));
}
else
{
_tm_state = TM_SETUP_FAILED;
logger.log_string(String("ERROR: Timezone and/or NTP-Server were not given correctly!"));
}
}
void TimeManager::time_set_cb(void)
{
if ((*_is_wifi_connected)())
{
if (get_time() == TIME_UPDATE_OK)
{
_ntp_sync_timestamp_s = _now;
logger.log_string("NTP successfully synced at...");
log_time();
}
else
{
logger.log_string("NTP sync failed!");
}
}
else
{
logger.log_string("NTP sync failed, WiFi is not connected!");
}
}

View File

@@ -1,8 +1,6 @@
#include "udp_logger.h"
UDPLogger::UDPLogger()
{
}
UDPLogger::UDPLogger() {}
UDPLogger::UDPLogger(IPAddress interface_addr, IPAddress multicast_addr, int port, String name)
{
@@ -26,8 +24,10 @@ void UDPLogger::log_string(String message)
delay(10);
}
message = _name + ": " + message;
#ifdef SERIAL_DEBUG
Serial.println(message);
delay(10);
#endif /* SERIAL_DEBUG */
_udp.beginPacketMulticast(_multicastAddr, _port, _interfaceAddr);
message.toCharArray(_packetBuffer, 100);
_udp.print(_packetBuffer);

View File

@@ -10,6 +10,8 @@ extern LEDMatrix led_matrix;
const int8_t dx[] = {1, -1, 0, 0};
const int8_t dy[] = {0, 0, -1, 1};
bool spiral_direction = false; // Direction of sprial animation
/**
* @brief Function to draw a spiral step (from center)
*

View File

@@ -21,7 +21,7 @@ const uint32_t colors_24bit[NUM_COLORS] = {
* @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)
LEDMatrix::LEDMatrix(FastLED_NeoMatrix *matrix, uint8_t brightness, UDPLogger *logger)
{
_neomatrix = matrix;
_brightness = brightness;
@@ -181,6 +181,11 @@ void LEDMatrix::flush(void)
_target_minute_indicators[3] = 0;
}
uint16_t LEDMatrix::get_fps(void)
{
return FastLED.getFPS();
}
/**
* @brief Write target pixels directly to leds
*

View File

@@ -179,58 +179,79 @@ String time_to_string(uint8_t hours, uint8_t minutes)
// show hours
switch (hours)
{
case 0:
if (minutes >= 0 && minutes < 5)
case 0:
{
message += "MITTERNACHT ";
if (minutes >= 0 && minutes < 5)
{
message += "MITTERNACHT ";
}
else
{
message += "ZWOLF ";
}
break;
}
else
case 1:
{
message += "EIN";
message += (minutes > 4) ? "S " : " "; // add "S" if needed
break;
}
case 2:
{
message += "ZWEI ";
break;
}
case 3:
{
message += "DREI ";
break;
}
case 4:
{
message += "VIER ";
break;
}
case 5:
{
message += "FUNF ";
break;
}
case 6:
{
message += "SECHS ";
break;
}
case 7:
{
message += "SIEBEN ";
break;
}
case 8:
{
message += "ACHT ";
break;
}
case 9:
{
message += "NEUN ";
break;
}
case 10:
{
message += "ZEHN ";
break;
}
case 11:
{
message += "ELF ";
break;
}
case 12:
{
message += "ZWOLF ";
break;
}
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;
case 12:
message += "ZWOLF ";
break;
}
if ((minutes < 5) && (hours != 0))
{

View File

@@ -23,72 +23,71 @@
#include <Arduino.h>
#include "wordclock_esp8266.h"
#include <Adafruit_GFX.h> // https://github.com/adafruit/Adafruit-GFX-Library
#include <Adafruit_NeoMatrix.h> // https://github.com/adafruit/Adafruit_NeoMatrix
#include <Adafruit_NeoPixel.h> // NeoPixel library used to run the NeoPixel LEDs: https://github.com/adafruit/Adafruit_NeoPixel
#include <EEPROM.h> //from ESP8266 Arduino Core (automatically installed when ESP8266 was installed via Boardmanager)
#include <Adafruit_GFX.h> // https://github.com/adafruit/Adafruit-GFX-Library
#include <base64.hpp>
#include <EEPROM.h> // from ESP8266 Arduino Core (automatically installed when ESP8266 was installed via Boardmanager)
#include <ESP8266WebServer.h>
#include <ESP8266WiFi.h>
#include <WiFiManager.h> //https://github.com/tzapu/WiFiManager WiFi Configuration Magic
#include <WiFiUdp.h>
#include <FastLED.h>
#include <WiFiManager.h> // https://github.com/tzapu/WiFiManager WiFi Configuration Magic
// own libraries
#include "animation_functions.h"
#include "base64_wrapper.h" // copied from https://github.com/Xander-Electronics/Base64
#include "diagnosis.h"
#include "led_matrix.h"
#include "littlefs_wrapper.h"
#include "ntp_client_plus.h"
#include "ota_functions.h"
#include "pong.h"
#include "render_functions.h"
#include "snake.h"
#include "tetris.h"
#include "time_manager.h"
#include "udp_logger.h"
#include "wordclock_constants.h"
// ----------------------------------------------------------------------------------
// GLOBAL VARIABLES
// ----------------------------------------------------------------------------------
Adafruit_NeoMatrix matrix = Adafruit_NeoMatrix(MATRIX_WIDTH, MATRIX_HEIGHT + 1, NEOPIXEL_PIN,
NEO_MATRIX_TOP + NEO_MATRIX_LEFT + NEO_MATRIX_ROWS + NEO_MATRIX_ZIGZAG,
NEO_GRB + NEO_KHZ800); // NeoMatrix
UDPLogger logger; // Logger
UDPLogger logger; // Global UDP logger instance
CRGB leds[NUM_MATRIX]; // LED array for FastLED
FastLED_NeoMatrix matrix = FastLED_NeoMatrix(leds, MATRIX_WIDTH, (MATRIX_HEIGHT + 1),
NEO_MATRIX_TOP + NEO_MATRIX_LEFT + NEO_MATRIX_ROWS + NEO_MATRIX_ZIGZAG);
LEDMatrix led_matrix = LEDMatrix(&matrix, DEFAULT_BRIGHTNESS, &logger); // FastLED_NeoMatrix wrapper
ESP8266WebServer webserver(HTTP_PORT); // Webserver
LEDMatrix led_matrix = LEDMatrix(&matrix, DEFAULT_BRIGHTNESS, &logger); // NeoMatrix wrapper
WiFiUDP wifi_udp;
NTPClientPlus ntp_client = NTPClientPlus(wifi_udp, NTP_SERVER_URL, 1, true);
// ----------------------------------------------------------------------------------
// STATIC VARIABLES
// ----------------------------------------------------------------------------------
// EEPROM values
static EepromLayout_st eeprom_buffer = {{0, 0, 0, 0}, {0U, 0U, 0U, false}, {0U, 0U, 0U, 0U}};
static Brightness_st *brightness_ps = &eeprom_buffer.brightness_values;
static Color_st *colors_ps = &eeprom_buffer.color_values;
static NightModeTimes_st *night_mode_times_ps = &eeprom_buffer.night_mode_times;
static Brightness_st *const brightness_ps = &eeprom_buffer.brightness_values;
static Color_st *const colors_ps = &eeprom_buffer.color_values;
static NightModeTimes_st *const night_mode_times_ps = &eeprom_buffer.night_mode_times;
// Games
static Pong pong = Pong(&led_matrix, &logger);
static Snake snake = Snake(&led_matrix, &logger);
static Tetris tetris = Tetris(&led_matrix, &logger);
static bool flg_night_mode = false; // state of nightmode
static bool flg_reset_wifi_creds = false; // used to reset stored wifi credentials
static bool spiral_direction = false;
static float filter_factor = DEFAULT_SMOOTHING_FACTOR; // stores smoothing factor for led transition
static int watchdog_counter = 30; // Watchdog counter to trigger restart if NTP update was not possible 30 times in a row (5min)
static uint32 last_led_direct_us = 0; // time of last direct LED command (=> fall back to normal mode after timeout)
static uint32_t heartbeat_counter = 0; // for heartbeat on-time in seconds
static uint32_t main_color_clock = colors_24bit[2]; // color of the clock and digital clock
static uint32_t main_color_snake = colors_24bit[1]; // color of the random snake animation
static uint8_t current_brightness = DEFAULT_BRIGHTNESS; // current brightness of LEDs
static uint8_t current_state = (uint8_t)ST_CLOCK; // stores current state
// Time Manager
static TimeManager tm_mgr;
static const String state_names[NUM_STATES] = {"Clock", "DiClock", "Spiral", "Tetris", "Snake", "PingPong", "Hearts"};
static const uint32_t period_timings[NUM_STATES] = {PERIOD_CLOCK_UPDATE_US, PERIOD_CLOCK_UPDATE_US,
PERIOD_ANIMATION_US, PERIOD_TETRIS_US, PERIOD_SNAKE_US,
PERIOD_PONG_US, PERIOD_ANIMATION_US};
// State variables
static bool flg_night_mode = false; // State of nightmode
static bool flg_reset_wifi_creds = false; // Used to reset stored wifi credentials
static float filter_factor = DEFAULT_SMOOTHING_FACTOR; // Stores smoothing factor for led transition, value of 1 represents no smoothing.
static uint32_t main_color_clock = colors_24bit[2]; // Color of the clock and digital clock
static uint8_t current_brightness = DEFAULT_BRIGHTNESS; // Current brightness of LEDs
static ClockState_en current_state = ST_CLOCK; // Stores current state
// Quarterly brightness factor for dynamic brightness
static const float qtly_brightness_factor[96] = {
// Other variables
static uint32 last_led_direct_us = 0; // Time of last direct LED command (=> fall back to normal mode after timeout)
static uint32_t heartbeat_counter = 0; // Heartbeat on-time in seconds
// Const definitions
static const String state_names[NUM_STATES] = {"Clock", "DiClock", "Spiral", "Tetris", "Snake", "PingPong", "Hearts"}; // all clock states
static const float qtly_brightness_factor[96] = { // Quarterly brightness factor for dynamic brightness (4 quarters a 24 hours)
0.0f, 0.0f, 0.0f, 0.001f, 0.003f, 0.007f, 0.014f, 0.026f, 0.044f, 0.069f, 0.101f, 0.143f, 0.194f, 0.253f, 0.32f,
0.392f, 0.468f, 0.545f, 0.62f, 0.691f, 0.755f, 0.811f, 0.858f, 0.896f, 0.927f, 0.949f, 0.966f, 0.978f, 0.986f,
0.991f, 0.995f, 0.997f, 0.998f, 0.999f, 0.999f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,
@@ -96,6 +95,9 @@ static const float qtly_brightness_factor[96] = {
0.998f, 0.997f, 0.995f, 0.991f, 0.986f, 0.978f, 0.966f, 0.949f, 0.927f, 0.896f, 0.858f, 0.811f, 0.755f, 0.691f,
0.62f, 0.545f, 0.468f, 0.392f, 0.32f, 0.253f, 0.194f, 0.143f, 0.101f, 0.069f, 0.044f, 0.026f, 0.014f, 0.007f,
0.003f, 0.001f, 0.0f, 0.0f};
static const uint32_t period_timings[NUM_STATES] = {PERIOD_TIME_UPDATE_US, PERIOD_TIME_UPDATE_US,
PERIOD_ANIMATION_US, PERIOD_TETRIS_US, PERIOD_SNAKE_US,
PERIOD_PONG_US, PERIOD_ANIMATION_US};
// ----------------------------------------------------------------------------------
// SETUP
@@ -110,12 +112,15 @@ void setup()
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);
rst_info *reset_info = ESP.getResetInfoPtr();
Serial.printf("Reset reason: %u\n", reset_info->reason);
Serial.printf("Reset cause: %u\n", reset_info->exccause);
Serial.printf("Reset address: %u\n", reset_info->excvaddr);
Serial.println();
// Init FastLED
FastLED.addLeds<WS2812B, FASTLED_PIN, COLOR_ORDER>(leds, NUM_MATRIX);
// Init EEPROM
EEPROM.begin(EEPROM_SIZE);
@@ -132,8 +137,8 @@ void setup()
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]);
// Turn on minutes LEDs (blue)
led_matrix.set_min_indicator((uint8_t)0b1111, colors_24bit[6]);
led_matrix.draw_on_matrix_instant();
/* Use WiFiMaanger for handling initial Wifi setup */
@@ -146,102 +151,64 @@ void setup()
// If you get here you have connected to the WiFi
Serial.printf("Connected, IP address: ");
Serial.println(WiFi.localIP());
// ESP8266 tries to reconnect automatically when the connection is lost
WiFi.setAutoReconnect(true);
WiFi.persistent(true);
// Turn off minutes leds
led_matrix.set_min_indicator(15, 0);
// Turn off minutes LEDs
led_matrix.set_min_indicator((uint8_t)0b1111, 0);
led_matrix.draw_on_matrix_instant();
// init ESP8266 File manager (LittleFS)
setup_filesystem();
// setup OTA
// set up OTA
setupOTA(HOSTNAME);
webserver.on("/cmd", handle_command); // process commands
webserver.on("/data", handle_data_request); // process data requests
webserver.on("/leddirect", HTTP_POST, handle_led_direct); // Call the 'handle_led_direct' function when a POST request is made to URI "/leddirect"
webserver.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)
if (reset_info->reason != REASON_SOFT_RESTART) // only if there was a cold start/hard reset
{
// quickly test each LED
for (int16_t row = 0; row < MATRIX_HEIGHT; row++)
{
for (int16_t col = 0; col < MATRIX_WIDTH; col++)
{
matrix.fillScreen(0);
matrix.drawPixel(col, row, LEDMatrix::color_24_to_16bit(colors_24bit[2]));
matrix.show();
delay(10);
}
}
// clear Matrix
matrix.fillScreen(0);
matrix.show();
delay(200);
// display IP
uint8_t address = WiFi.localIP()[3];
led_matrix.print_char(1, 0, 'I', main_color_clock);
led_matrix.print_char(5, 0, 'P', main_color_clock);
led_matrix.print_number(0, 6, (address / 100), main_color_clock);
led_matrix.print_number(4, 6, (address / 10) % 10, main_color_clock);
led_matrix.print_number(8, 6, address % 10, main_color_clock);
led_matrix.draw_on_matrix_instant();
delay(2000);
// clear matrix
led_matrix.flush();
led_matrix.draw_on_matrix_instant();
cold_start_setup();
}
// setup NTP
ntp_client.setup_ntp_client();
logger.log_string("NTP running");
logger.log_string("Time: " + ntp_client.get_formatted_time());
logger.log_string("TimeOffset (seconds): " + String(ntp_client.get_time_offset()));
// set up time manager and get initial time
tm_mgr = TimeManager(MY_TZ, NTP_SERVER_URL, check_wifi_status, NTP_MAX_OFFLINE_TIME_S, &logger);
tm_mgr.init();
// show the current time for short time in words
int hours = ntp_client.get_hours_24();
int minutes = ntp_client.get_minutes();
String timeMessage = time_to_string(hours, minutes);
show_string_on_clock(timeMessage, main_color_clock);
draw_minute_indicator(minutes, main_color_clock);
led_matrix.draw_on_matrix_smooth(filter_factor);
if (tm_mgr.get_time() == TIME_UPDATE_OK)
{
// show the current time for short time in words
String timeMessage = time_to_string(tm_mgr.hour(), tm_mgr.minute());
show_string_on_clock(timeMessage, main_color_clock);
draw_minute_indicator(tm_mgr.minute(), main_color_clock);
led_matrix.draw_on_matrix_smooth(filter_factor);
}
else
{
logger.log_string("Warning: Initial time sync failed! Retrying in a bit.");
}
// init all animation modes
// init snake
random_snake(true, 8, colors_24bit[1], -1);
// init spiral
draw_spiral(true, spiral_direction, MATRIX_WIDTH - 6);
// init random tetris
random_tetris(true);
// Range limits
// Set range limits
limit_value_ranges();
logger.log_string("Nightmode starts at: " + String(night_mode_times_ps->start_hour) + ":" + String(night_mode_times_ps->start_min));
logger.log_string("Nightmode ends at: " + String(night_mode_times_ps->end_hour) + ":" + String(night_mode_times_ps->end_min));
// Update brightness
current_brightness = update_brightness();
logger.log_string("Brightness: " + String(((uint16_t)current_brightness * 100) / UINT8_MAX) + "%");
// Send logging data
log_data();
}
// ----------------------------------------------------------------------------------
@@ -252,57 +219,61 @@ void loop()
uint32 current_time_us = system_get_time();
// Timestamp variables
static uint32 last_animation_step_us = 0; // time of last animation step
static uint32 last_matrix_update_us = 0; // time of last Matrix update
static uint32 last_heartbeat_us = 0; // time of last heartbeat sending
static uint32 last_nightmode_check_us = 0; // time of last nightmode check
static uint32 last_ntp_update_us = 0; // time of last NTP update
static uint32 last_animation_step_us = 0; // timestamp of last animation step
static uint32 last_matrix_update_us = 0; // timestamp of last Matrix update
static uint32 last_time_update_us = 0; // timestamp of last time update
static uint32 last_heartbeat_us = 0; // timestamp of last heartbeat sending
static uint32 last_nightmode_check_us = 0; // timestamp of last nightmode check
static uint32 last_brightness_update_us = 0; // timestamp of last brightness update
handleOTA(); // handle OTA
webserver.handleClient(); // handle webserver
handle_button(); // handle button press
// send regularly heartbeat messages via UDP multicast
if ((current_time_us - last_heartbeat_us) > PERIOD_HEARTBEAT_US)
if ((current_time_us - last_heartbeat_us) >= PERIOD_HEARTBEAT_US)
{
send_heartbeat(); // send heartbeat update
last_heartbeat_us = system_get_time();
delay(10);
}
if (!flg_night_mode && ((current_time_us - last_animation_step_us) > period_timings[current_state]) &&
((current_time_us - last_led_direct_us) > TIMEOUT_LEDDIRECT_US))
((current_time_us - last_led_direct_us) >= TIMEOUT_LEDDIRECT_US))
{
handle_current_state(); // handle current state
last_animation_step_us = system_get_time();
delay(10);
}
if ((current_time_us - last_matrix_update_us) > PERIOD_MATRIX_UPDATE_US)
if ((current_time_us - last_brightness_update_us) >= PERIOD_BRIGHTNESS_UPDATE_US)
{
current_brightness = update_brightness(); // update brightness
logger.log_string("Brightness: " + String(((uint16_t)current_brightness * 100) / UINT8_MAX) + "%");
last_brightness_update_us = system_get_time();
}
if ((current_time_us - last_matrix_update_us) >= PERIOD_MATRIX_UPDATE_US)
{
update_matrix(); // update matrix
last_matrix_update_us = system_get_time();
delay(10);
}
handle_button(); // handle button press
if ((current_time_us - last_ntp_update_us) > PERIOD_NTP_UPDATE_US)
if ((current_time_us - last_time_update_us) >= PERIOD_TIME_UPDATE_US)
{
check_wifi_status(); // check WiFi status before NTP update
delay(10);
(void)tm_mgr.get_time(); // NTP time update
ntp_time_update(&last_ntp_update_us); // NTP time update
delay(10);
if (tm_mgr.ntp_sync_timeout())
{
logger.log_string("Trigger restart due to being offline for too long...");
delay(100);
ESP.restart();
}
current_brightness = update_brightness(); // update brightness every PERIOD_NTP_UPDATE_US
delay(10);
logger.log_string("Brightness: " + String(((uint16_t)current_brightness * 100) / UINT8_MAX) + "%");
delay(10);
last_time_update_us = system_get_time();
}
if ((current_time_us - last_nightmode_check_us) > PERIOD_NIGHTMODE_CHECK_US)
if ((current_time_us - last_nightmode_check_us) >= PERIOD_NIGHTMODE_CHECK_US)
{
check_night_mode(); // check night mode
last_nightmode_check_us = system_get_time();
@@ -312,25 +283,105 @@ void loop()
// ----------------------------------------------------------------------------------
// OTHER FUNCTIONS
// ----------------------------------------------------------------------------------
/**
* @brief Log information
*
*/
void log_data()
{
logger.log_string("Start program\n");
logger.log_string("Sketchname: " + String(__FILE__));
logger.log_string("Build: " + String(__TIMESTAMP__));
logger.log_string("IP: " + WiFi.localIP().toString());
logger.log_string("Reset Reason: " + ESP.getResetReason());
logger.log_string("Nightmode starts at: " + String(night_mode_times_ps->start_hour) + ":" + String(night_mode_times_ps->start_min));
logger.log_string("Nightmode ends at: " + String(night_mode_times_ps->end_hour) + ":" + String(night_mode_times_ps->end_min));
logger.log_string("Brightness: " + String(((uint16_t)current_brightness * 100) / UINT8_MAX) + "%\n");
}
/**
* @brief Test all LEDs and display IP address
*
*/
void cold_start_setup()
{
// quickly test each LED
for (int16_t row = 0; row < MATRIX_HEIGHT; row++)
{
for (int16_t col = 0; col < MATRIX_WIDTH; col++)
{
matrix.fillScreen(0);
matrix.drawPixel(col, row, LEDMatrix::color_24_to_16bit(colors_24bit[2]));
matrix.show();
delay(10);
}
}
// clear Matrix
matrix.fillScreen(0);
matrix.show();
delay(200);
// display IP
uint8_t address = WiFi.localIP()[3];
led_matrix.print_char(1, 0, 'I', main_color_clock);
led_matrix.print_char(5, 0, 'P', main_color_clock);
led_matrix.print_number(0, 6, (address / 100), main_color_clock);
led_matrix.print_number(4, 6, (address / 10) % 10, main_color_clock);
led_matrix.print_number(8, 6, address % 10, main_color_clock);
led_matrix.draw_on_matrix_instant();
delay(2000);
// clear matrix
led_matrix.flush();
led_matrix.draw_on_matrix_instant();
}
/**
* @brief Update and control word clock states.
*/
void handle_current_state()
{
switch (current_state)
{
case ST_CLOCK: // state clock
{
int hours = ntp_client.get_hours_24();
int minutes = ntp_client.get_minutes();
(void)show_string_on_clock(time_to_string((uint8_t)hours, (uint8_t)minutes), main_color_clock);
draw_minute_indicator((uint8_t)minutes, main_color_clock);
if (tm_mgr.tm_state() == TM_NORMAL)
{
(void)show_string_on_clock(time_to_string((uint8_t)tm_mgr.hour(), (uint8_t)tm_mgr.minute()), main_color_clock);
draw_minute_indicator((uint8_t)tm_mgr.minute(), main_color_clock);
}
else if (tm_mgr.ntp_sync_overdue()) // if NTP sync is overdue
{
(void)show_string_on_clock(time_to_string((uint8_t)tm_mgr.hour(), (uint8_t)tm_mgr.minute()), main_color_clock);
draw_minute_indicator((uint8_t)tm_mgr.minute(), colors_24bit[6]); // in blue to indicate a network problem
}
else // if no NTP sync has been done, only show 4 blue minute indicators
{
// clear matrix
led_matrix.flush();
// Turn on minutes LEDs (blue)
led_matrix.set_min_indicator((uint8_t)0b1111, colors_24bit[6]);
led_matrix.draw_on_matrix_instant();
}
break;
}
case ST_DICLOCK: // state diclock
{
int hours = ntp_client.get_hours_24();
int minutes = ntp_client.get_minutes();
show_digital_clock((uint8_t)hours, (uint8_t)minutes, main_color_clock);
if (tm_mgr.ntp_sync_successful())
{
show_digital_clock((uint8_t)tm_mgr.hour(), (uint8_t)tm_mgr.minute(), main_color_clock);
}
else
{
// clear matrix
led_matrix.flush();
// Turn on minutes LEDs (blue)
led_matrix.set_min_indicator((uint8_t)0b1111, colors_24bit[6]);
led_matrix.draw_on_matrix_instant();
}
break;
}
case ST_SPIRAL: // state spiral
@@ -338,14 +389,14 @@ void handle_current_state()
int res = draw_spiral(false, spiral_direction, MATRIX_WIDTH - 2);
if ((bool)res && spiral_direction == 0)
{
// change spiral direction to closing (draw empty leds)
// change spiral direction to closing (draw empty LEDs)
spiral_direction = true;
// init spiral with new spiral direction
draw_spiral(true, spiral_direction, MATRIX_WIDTH - 1);
}
else if (res && spiral_direction == 1)
{
// reset spiral direction to normal drawing leds
// reset spiral direction to normal drawing LEDs
spiral_direction = false;
// init spiral with new spiral direction
draw_spiral(true, spiral_direction, MATRIX_WIDTH - 1);
@@ -406,14 +457,18 @@ void send_heartbeat()
* @brief Check WiFi status and try to reconnect if needed. Should be called in loop() before NTP update.
*
* @param None
*
* @retval bool - true if WiFi is connected, false otherwise
*/
void check_wifi_status()
bool check_wifi_status()
{
bool connected = (WiFi.status() == WL_CONNECTED);
// Check wifi status
if (WiFi.status() != WL_CONNECTED)
if (!connected)
{
Serial.println("WiFi connection lost! Trying to reconnect automatically...");
}
return connected;
}
/**
@@ -423,9 +478,9 @@ void check_wifi_status()
*/
void check_night_mode()
{
// check if nightmode need to be activated
int hours = ntp_client.get_hours_24();
int minutes = ntp_client.get_minutes();
// Check if nightmode needs to be activated. This only toggles at the exact minute.
int hours = tm_mgr.hour();
int minutes = tm_mgr.minute();
if ((hours == night_mode_times_ps->start_hour) && (minutes == night_mode_times_ps->start_min))
{
@@ -437,63 +492,6 @@ void check_night_mode()
}
}
/**
* @brief NTP time update, should be called in loop().
*
* @param None
*/
void ntp_time_update(uint32 *last_ntp_update_us)
{
// NTP time update
int ntp_retval = ntp_client.update_ntp();
switch (ntp_retval)
{
case NTP_UPDATE_SUCCESS:
{
ntp_client.calc_date();
logger.log_string("NTP-Update successful, Time: " + ntp_client.get_formatted_time());
*last_ntp_update_us = system_get_time();
watchdog_counter = 30;
break;
}
case NTP_UPDATE_TIMEOUT:
{
logger.log_string("NTP-Update not successful. Reason: Timeout");
*last_ntp_update_us += 10000000;
watchdog_counter--;
break;
}
case NTP_UPDATE_DIFFTOOHIGH:
{
logger.log_string("NTP-Update not successful. Reason: Too large time difference");
logger.log_string("Time: " + ntp_client.get_formatted_time());
logger.log_string("Date: " + ntp_client.get_formatted_date());
logger.log_string("TimeOffset (seconds): " + String(ntp_client.get_time_offset()));
logger.log_string("Summertime: " + String(ntp_client.check_daylight_saving_time()));
*last_ntp_update_us += 10000000;
watchdog_counter--;
break;
}
case NTP_UPDATE_TIME_INVALID:
default:
{
logger.log_string("NTP-Update not successful. Reason: NTP time not valid (<1970)");
*last_ntp_update_us += 10000000;
watchdog_counter--;
break;
}
}
logger.log_string("Watchdog counter: " + String(watchdog_counter));
if (watchdog_counter <= 0)
{
logger.log_string("Trigger restart due to watchdog...");
delay(100);
ESP.restart();
}
}
/**
* @brief call entry action of given state
*
@@ -501,7 +499,7 @@ void ntp_time_update(uint32 *last_ntp_update_us)
*/
void on_state_entry(uint8_t state)
{
filter_factor = 0.5f;
filter_factor = DEFAULT_SMOOTHING_FACTOR;
switch (state)
{
case ST_SPIRAL:
@@ -536,21 +534,21 @@ void on_state_entry(uint8_t state)
}
/**
* @brief execute a state change to given newState
* @brief execute a state change to given new_state
*
* @param newState the new state to be changed to
* @param new_state the new state to be changed to
*/
void state_change(uint8_t newState)
void state_change(ClockState_en new_state)
{
if (flg_night_mode)
{
set_night_mode(false); // deactivate Nightmode
}
led_matrix.flush(); // first clear matrix
current_state = newState; // set new state
on_state_entry(current_state);
logger.log_string("State change to: " + state_names[current_state]);
led_matrix.flush(); // first clear matrix
current_state = new_state; // set new state
on_state_entry((uint8_t)current_state);
logger.log_string("State change to: " + state_names[(uint8_t)current_state]);
logger.log_string("FreeMemory=" + String(ESP.getFreeHeap()));
}
@@ -579,10 +577,9 @@ void handle_led_direct()
// 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);
unsigned int decodedLength = decode_base64_length((unsigned char *)base64data, dataLength);
unsigned char byteArray[decodedLength];
decode_base64((unsigned char *)base64data, dataLength, byteArray);
for (unsigned int i = 0; i < dataLength; i += 4)
{
@@ -643,7 +640,7 @@ void handle_button()
}
else
{
state_change((current_state + 1) % (uint8_t)NUM_STATES);
state_change((ClockState_en)(((uint8_t)current_state + 1) % (uint8_t)NUM_STATES));
}
}
}
@@ -658,11 +655,11 @@ void handle_button()
void set_main_color(uint8_t red, uint8_t green, uint8_t blue)
{
main_color_clock = LEDMatrix::color_24bit(red, green, blue);
// Update colors and save color settings to EEPROM
colors_ps->blue = blue;
colors_ps->red = red;
colors_ps->green = green;
// save color settings to EEPROM
write_settings_to_EEPROM();
}
@@ -693,13 +690,16 @@ void draw_main_color()
*/
void handle_command()
{
bool send204 = true; // flag to send 204 response
// receive command and handle accordingly
#ifdef SERIAL_DEBUG
for (uint8_t i = 0; i < webserver.args(); i++)
{
Serial.print(webserver.argName(i));
Serial.print(F(": "));
Serial.println(webserver.arg(i));
}
#endif /* SERIAL_DEBUG */
if (webserver.argName(0).equals("led")) // the parameter which was sent to this server is led color
{
@@ -718,6 +718,7 @@ void handle_command()
{
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"))
{
@@ -799,7 +800,6 @@ void handle_command()
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();
@@ -828,7 +828,6 @@ void handle_command()
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();
@@ -853,7 +852,6 @@ void handle_command()
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);
@@ -867,8 +865,18 @@ void handle_command()
pong.initGame(1);
}
}
else if (webserver.argName(0).equals("diag"))
{
String cmd_str = webserver.arg(0);
Diagnosis diag(&logger, &led_matrix);
webserver.send(200, "text/plain", diag.handle_command(cmd_str));
send204 = false;
}
webserver.send(204, "text/plain", "No Content"); // this page doesn't send back content --> 204
if (send204)
{
webserver.send(204, "text/plain", "No Content"); // this page doesn't send back content --> 204
}
}
/**
@@ -877,6 +885,7 @@ void handle_command()
*/
void handle_data_request()
{
#ifdef SERIAL_DEBUG
// receive data request and handle accordingly
for (uint8_t i = 0; i < webserver.args(); i++)
{
@@ -884,7 +893,7 @@ void handle_data_request()
Serial.print(F(": "));
Serial.println(webserver.arg(i));
}
#endif /* SERIAL_DEBUG */
if (webserver.argName(0).equals("key"))
{
String message = "{";
@@ -1032,9 +1041,9 @@ uint8_t update_brightness()
{
new_brightness = calculate_dynamic_brightness(brightness_ps->dyn_brightness_min,
brightness_ps->dyn_brightness_max,
ntp_client.get_hours_24(),
ntp_client.get_minutes(),
ntp_client.check_daylight_saving_time());
tm_mgr.hour(),
tm_mgr.minute(),
tm_mgr.isdst());
}
else // use static brightness
{

View File

@@ -1,143 +0,0 @@
/*
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 <Arduino.h>
#include "base64_wrapper.h"
#if (defined(__AVR__))
#include <avr/pgmspace.h>
#else
#include <pgmspace.h>
#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;