#include <WiFiManager.h> // https://github.com/tzapu/WiFiManager
#include <time.h>
#include <LittleFS.h>
#include <PNGdec.h>
#include "SPI.h"
#include <TFT_eSPI.h> // Hardware-specific library
#define TRIGGER_PIN 0
#define FileSys LittleFS
#define MAX_IMAGE_WIDTH 240 // Adjust for your images
int TFT_CS1 = 15; // Chip select control pin Screen 1
int TFT_CS2 = 17; // Chip select control pin Screen 2
int TFT_CS3 = 21; // Chip select control pin Screen 3
int TFT_CS4 = 22; // Chip select control pin Screen 4
PNG png;
int16_t xpos = 0;
int16_t ypos = 0;
bool wm_nonblocking = false; // change to true to use non blocking
WiFiManager wm; // global wm instance
WiFiManagerParameter custom_field; // global param ( for non blocking w params )
// Set web server port number to 80
WiFiServer server(80);
// Variable to store the HTTP request
String header;
int timeout = 120; // seconds to run for
TFT_eSPI tft = TFT_eSPI(); // Invoke custom library
String currentNumberSet = "a";
const char* NTP_SERVER = "pool.ntp.org";
const char* TZ_INFO = "GMT+0IST-1,M3.5.0/01:00:00,M10.5.0/02:00:00"; // enter your time zone (https://remotemonitoringsystems.ca/time-zone-abbreviations.php)
tm timeinfo;
time_t now;
long unsigned lastNTPtime;
unsigned long lastEntryTime;
// Current time
unsigned long currentTime = millis();
// Previous time
unsigned long previousTime = 0;
// Define timeout time in milliseconds (example: 2000ms = 2s)
const long timeoutTime = 2000;
uint32_t targetTime = 0;
static uint8_t conv2d(const char* p); // Forward declaration needed for IDE 1.6.x
uint8_t hh = conv2d(__TIME__), mm = conv2d(__TIME__ + 3), ss = conv2d(__TIME__ + 6); // Get H, M, S from compile time
byte omm = 99, oss = 99;
byte xcolon = 0, xsecs = 0;
int colour = 0;
unsigned int currentUnit = 0;
unsigned int currentTen = 0;
void setup() {
// put your setup code here, to run once:
Serial.begin(115200);
Serial.setDebugOutput(true);
pinMode(TFT_CS1, OUTPUT);
pinMode(TFT_CS2, OUTPUT);
pinMode(TFT_CS3, OUTPUT);
pinMode(TFT_CS4, OUTPUT);
digitalWrite(TFT_CS1, LOW);
digitalWrite(TFT_CS2, LOW);
digitalWrite(TFT_CS3, LOW);
digitalWrite(TFT_CS4, LOW);
delay(100);
tft.init();
tft.setTextSize(1);
tft.setRotation(0);
tft.fillScreen(TFT_BLACK);
tft.setTextColor(0x1FFE);
digitalWrite(TFT_CS1, HIGH);
digitalWrite(TFT_CS2, HIGH);
digitalWrite(TFT_CS3, HIGH);
digitalWrite(TFT_CS4, HIGH);
delay(100);
loggerln(".");
loggerln(".");
loggerln(".");
loggerln(".");
loggerln("Setting pins high");
loggerln("initalize with black screens");
screenPrintln(TFT_CS1,"Screen 1");
screenPrintln(TFT_CS2,"Screen 2");
screenPrintln(TFT_CS3,"Screen 3");
screenPrintln(TFT_CS4,"Screen 4");
WiFi.mode(WIFI_STA); // explicitly set mode, esp defaults to STA+AP
loggerln("\n\n Begin Setup");
loggerln("\n Starting");
pinMode(TRIGGER_PIN, INPUT_PULLUP);
wifiManagerInit();
configTime(0, 0, NTP_SERVER);
// See https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv for Timezone codes for your region
setenv("TZ", TZ_INFO, 1);
// Initialise FS
loggerln("Initialise LittleFS!");
if (!FileSys.begin()) {
loggerln("LittleFS initialisation failed!");
while (1) yield(); // Stay here twiddling thumbs waiting
}
if (getNTPtime(10)) { // wait up to 10sec to sync
loggerln("Time updated from NTP");
} else {
loggerln("Failed to connect to NTP");
}
targetTime = millis() + 1000;
}
void loop() {
// put your main code here, to run repeatedly:
if (wm_nonblocking) wm.process(); // avoid delays() in loop when non-blocking and other long running code
checkButton();
webServerLoop();
secLoop();
}
void logger(char* message) {
//output to both serial and tft screen
int x;
int y;
x = tft.getCursorX();
y = tft.getCursorY();
Serial.print(message);
digitalWrite(TFT_CS1, LOW);
digitalWrite(TFT_CS2, LOW);
digitalWrite(TFT_CS3, LOW);
digitalWrite(TFT_CS4, LOW);
delay(100);
tft.setTextColor(0x1FFE, TFT_BLACK);
tft.setCursor(x, y);
tft.print(message);
digitalWrite(TFT_CS1, HIGH);
digitalWrite(TFT_CS2, HIGH);
digitalWrite(TFT_CS3, HIGH);
digitalWrite(TFT_CS4, HIGH);
delay(100);
}
void loggerln(char* message) {
//output to both serial and tft screen
int x;
int y;
x = tft.getCursorX();
y = tft.getCursorY();
Serial.println(message);
digitalWrite(TFT_CS1, LOW);
digitalWrite(TFT_CS2, LOW);
digitalWrite(TFT_CS3, LOW);
digitalWrite(TFT_CS4, LOW);
delay(100);
tft.setTextColor(0x1FFE, TFT_BLACK);
tft.setCursor(x, y);
tft.println(message);
digitalWrite(TFT_CS1, HIGH);
digitalWrite(TFT_CS2, HIGH);
digitalWrite(TFT_CS3, HIGH);
digitalWrite(TFT_CS4, HIGH);
delay(100);
}
void screenPrintln(int CS, char* message) {
//output to selected tft screen
int x;
int y;
x = tft.getCursorX();
y = tft.getCursorY();
Serial.println(message);
digitalWrite(CS, LOW);
delay(100);
tft.setTextColor(0x1FFE, TFT_BLACK);
tft.setCursor(x, y);
tft.println(message);
digitalWrite(CS, HIGH);
delay(100);
}
void webServerLoop() {
WiFiClient client = server.available(); // Listen for incoming clients
if (client) { // If a new client connects,
currentTime = millis();
previousTime = currentTime;
Serial.println("New Client."); // print a message out in the serial port
String currentLine = ""; // make a String to hold incoming data from the client
while (client.connected() && currentTime - previousTime <= timeoutTime) { // loop while the client's connected
currentTime = millis();
if (client.available()) { // if there's bytes to read from the client,
char c = client.read(); // read a byte, then
Serial.write(c); // print it out the serial monitor
header += c;
if (c == '\n') { // if the byte is a newline character
// if the current line is blank, you got two newline characters in a row.
// that's the end of the client HTTP request, so send a response:
if (currentLine.length() == 0) {
// HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
// and a content-type so the client knows what's coming, then a blank line:
client.println("HTTP/1.1 200 OK");
client.println("Content-type:text/html");
client.println("Connection: close");
client.println();
// Select the number set
if (header.indexOf("numSet=setA") >= 0) {
Serial.println("Set A");
currentNumberSet = "a";
} else if (header.indexOf("numSet=setB") >= 0) {
Serial.println("Set B");
currentNumberSet = "b";
} else if (header.indexOf("numSet=setC") >= 0) {
Serial.println("Set C");
currentNumberSet = "c";
} else if (header.indexOf("numSet=setD") >= 0) {
Serial.println("Set D");
currentNumberSet = "d";
}
// Display the HTML web page
client.println("<!DOCTYPE html><html>");
client.println("<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
client.println("<link rel=\"icon\" href=\"data:,\">");
// CSS to style the on/off buttons
// Feel free to change the background-color and font-size attributes to fit your preferences
client.println("<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}");
client.println(".button { background-color: #4CAF50; border: none; color: white; padding: 16px 40px;");
client.println("text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}");
client.println(".button2 {background-color: #555555;}</style></head>");
// Web Page Heading
client.println("<body><h1>Nixie Clock Settings</h1><form>");
client.print("<div><input type='radio' id='setA' name='numSet' value='setA' ");
if (currentNumberSet == "a") { client.print("checked"); }
client.println("><label for='setA'>Number Set A</label></div>");
client.print("<div><input type='radio' id='setB' name='numSet' value='setB' ");
if (currentNumberSet == "b") { client.print("checked"); }
client.println("><label for='setB'>Number Set B</label></div>");
client.print("<div><input type='radio' id='setC' name='numSet' value='setC' ");
if (currentNumberSet == "c") { client.print("checked"); }
client.println("><label for='setC'>Number Set C</label></div>");
client.print("<div><input type='radio' id='setD' name='numSet' value='setD' ");
if (currentNumberSet == "d") { client.print("checked"); }
client.println("><label for='setC'>Number Set D</label></div>");
client.println("<p><a href=\"/\"><button class=\"button\">Update</button></a></p>");
client.println("</form></body></html>");
// The HTTP response ends with another blank line
client.println();
// Break out of the while loop
break;
} else { // if you got a newline, then clear currentLine
currentLine = "";
}
} else if (c != '\r') { // if you got anything else but a carriage return character,
currentLine += c; // add it to the end of the currentLine
}
}
}
// Clear the header variable
header = "";
// Close the connection
client.stop();
Serial.println("Client disconnected.");
Serial.println("");
}
}
void wifiManagerInit() {
if (wm_nonblocking) wm.setConfigPortalBlocking(false);
// add a custom input field
int customFieldLength = 40;
// test custom html(radio)
const char* custom_radio_str = "<br/><label for='customfieldid'>Custom Field Label</label><input type='radio' name='customfieldid' value='1' checked> One<br><input type='radio' name='customfieldid' value='2'> Two<br><input type='radio' name='customfieldid' value='3'> Three";
new (&custom_field) WiFiManagerParameter(custom_radio_str); // custom html input
wm.addParameter(&custom_field);
wm.setSaveParamsCallback(saveParamCallback);
// custom menu via array or vector
//
// menu tokens, "wifi","wifinoscan","info","param","close","sep","erase","restart","exit" (sep is seperator) (if param is in menu, params will not show up in wifi page!)
// const char* menu[] = {"wifi","info","param","sep","restart","exit"};
// wm.setMenu(menu,6);
std::vector<const char*> menu = { "wifi", "info", "param", "sep", "restart", "exit" };
wm.setMenu(menu);
// set dark theme
wm.setClass("invert");
// set configportal timeout
wm.setConfigPortalTimeout(timeout);
bool res;
res = wm.autoConnect("NixieClock", "password"); // password protected ap
if (!res) {
loggerln("Failed to connect or hit timeout");
} else {
//if you get here you have connected to the WiFi
loggerln("connected to WiFi");
}
server.begin();
loggerln("\r\nInitialisation done.");
}
String getParam(String name) {
//read parameter from server, for customhmtl input
String value;
if (wm.server->hasArg(name)) {
value = wm.server->arg(name);
}
return value;
}
void saveParamCallback() {
Serial.println("[CALLBACK] saveParamCallback fired");
Serial.println("PARAM customfieldid = " + getParam("customfieldid"));
}
void checkButton() {
// is configuration portal requested?
if (digitalRead(TRIGGER_PIN) == LOW) {
// poor mans debounce/press-hold, code not ideal for production
delay(50);
if (digitalRead(TRIGGER_PIN) == LOW) {
loggerln("Button pressed.");
// still holding button for 3000 ms, reset settings, code not ideaa for production
delay(3000); // reset delay hold
if (digitalRead(TRIGGER_PIN) == LOW) {
loggerln("Button Held");
loggerln("Erasing Config, restarting");
wm.resetSettings();
ESP.restart();
}
// start portal w delay
loggerln("Starting config portal");
wm.setConfigPortalTimeout(120);
if (!wm.startConfigPortal("NixieClock", "password")) {
loggerln("failed to connect or hit timeout");
delay(3000);
// ESP.restart();
} else {
//if you get here you have connected to the WiFi
loggerln("connected");
if (getNTPtime(10)) { // wait up to 10sec to sync
loggerln("Time updated from NTP");
} else {
loggerln("Time not set");
}
}
}
}
}
bool getNTPtime(int sec) {
{
uint32_t start = millis();
do {
time(&now);
localtime_r(&now, &timeinfo);
Serial.print(".");
delay(10);
} while (((millis() - start) <= (1000 * sec)) && (timeinfo.tm_year < (2016 - 1900)));
if (timeinfo.tm_year <= (2016 - 1900)) return false; // the NTP call was not successful
Serial.print("now ");
Serial.println(now);
char time_output[30];
strftime(time_output, 30, "%a %d-%m-%y %T", localtime(&now));
Serial.println(time_output);
Serial.println();
}
return true;
}
void secLoop() {
int hhPad, mmPad, ssPad;
String hhTens, hhUnits, mmTens, mmUnits, ssTens,ssUnits;
if (targetTime < millis()) {
// Set next update for 1 second later
targetTime = millis() + 1000;
// Adjust the time values by adding 1 second
ss++; // Advance second
if (ss == 60) { // Check for roll-over
ss = 0; // Reset seconds to zero
omm = mm; // Save last minute time for display update
mm++; // Advance minute
if (mm > 59) { // Check for roll-over
mm = 0;
hh++; // Advance hour
if (hh > 23) { // Check for 24hr roll-over (could roll-over on 13)
hh = 0; // 0 for 24 hour clock, set to 1 for 12 hour clock
}
}
}
ssPad = 100 + ss;
ssTens = String(ssPad).substring(1, 2);
ssUnits = String(ssPad).substring(2, 3);
mmPad = 100 + mm;
mmTens = String(mmPad).substring(1, 2);
mmUnits = String(mmPad).substring(2, 3);
hhPad = 100 + hh;
hhTens = String(hhPad).substring(1, 2);
hhUnits = String(hhPad).substring(2, 3);
numToScreen2(1, hhTens.toInt());
numToScreen2(2, hhUnits.toInt());
numToScreen2(3, mmTens.toInt());
numToScreen2(4, mmUnits.toInt());
//Serial.println(targetTime);
//Serial.println(ssPad);
//Serial.println(tens);
//Serial.println(units);
//Serial.println(now);
}
}
void numToScreen2(int screenNo, int timeValue) {
int8_t CS;
switch (screenNo) {
case 1:
CS = TFT_CS1;
break;
case 2:
CS = TFT_CS2;
break;
case 3:
CS = TFT_CS3;
break;
case 4:
CS = TFT_CS4;
break;
}
digitalWrite(CS, LOW);
File root = LittleFS.open("/", "r");
String strname = "/";
strname += currentNumberSet;
strname += String(timeValue);
strname += ".png";
// Pass support callback function names to library
int16_t rc = png.open(strname.c_str(), pngOpen, pngClose, pngRead, pngSeek, pngDraw);
if (rc == PNG_SUCCESS) {
tft.startWrite();
//Serial.printf("image specs: (%d x %d), %d bpp, pixel type: %d\n", png.getWidth(), png.getHeight(), png.getBpp(), png.getPixelType());
//uint32_t dt = millis();
if (png.getWidth() > MAX_IMAGE_WIDTH) {
Serial.println("Image too wide for allocated line buffer size!");
} else {
rc = png.decode(NULL, 0);
png.close();
}
tft.endWrite();
// How long did rendering take...
//Serial.print(millis() - dt);
//Serial.println("ms");
}
digitalWrite(CS, HIGH);
}
void pngDraw(PNGDRAW* pDraw) {
uint16_t lineBuffer[MAX_IMAGE_WIDTH];
png.getLineAsRGB565(pDraw, lineBuffer, PNG_RGB565_BIG_ENDIAN, 0xffffffff);
tft.pushImage(xpos, ypos + pDraw->y, pDraw->iWidth, 1, lineBuffer);
}
// Function to extract numbers from compile time string
static uint8_t conv2d(const char* p) {
uint8_t v = 0;
if ('0' <= *p && *p <= '9')
v = *p - '0';
return 10 * v + *++p - '0';
}