Added "Show Date" display mode

Added "Show Date" display mode
This commit is contained in:
M-Factory
2025-08-14 11:48:02 +09:00
parent 0ca2d4f990
commit fdf42ebd4c
8 changed files with 516 additions and 37 deletions

View File

@@ -13,9 +13,10 @@
#include <time.h>
#include <WiFiClientSecure.h>
#include "mfactoryfont.h" // Custom font
#include "tz_lookup.h" // Timezone lookup, do not duplicate mapping here!
#include "days_lookup.h" // Languages for the Days of the Week
#include "mfactoryfont.h" // Custom font
#include "tz_lookup.h" // Timezone lookup, do not duplicate mapping here!
#include "days_lookup.h" // Languages for the Days of the Week
#include "months_lookup.h" // Languages for the Months of the Year
#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
#define MAX_DEVICES 4
@@ -49,6 +50,7 @@ int brightness = 7;
bool flipDisplay = false;
bool twelveHourToggle = false;
bool showDayOfWeek = true;
bool showDate = false;
bool showHumidity = false;
bool colonBlinkEnabled = true;
char ntpServer1[64] = "pool.ntp.org";
@@ -168,6 +170,7 @@ void loadConfig() {
doc[F("flipDisplay")] = flipDisplay;
doc[F("twelveHourToggle")] = twelveHourToggle;
doc[F("showDayOfWeek")] = showDayOfWeek;
doc[F("showDate")] = false;
doc[F("showHumidity")] = showHumidity;
doc[F("colonBlinkEnabled")] = colonBlinkEnabled;
doc[F("ntpServer1")] = ntpServer1;
@@ -233,8 +236,10 @@ void loadConfig() {
flipDisplay = doc["flipDisplay"] | false;
twelveHourToggle = doc["twelveHourToggle"] | false;
showDayOfWeek = doc["showDayOfWeek"] | true;
showDate = doc["showDate"] | false;
showHumidity = doc["showHumidity"] | false;
colonBlinkEnabled = doc.containsKey("colonBlinkEnabled") ? doc["colonBlinkEnabled"].as<bool>() : true;
showWeatherDescription = doc["showWeatherDescription"] | false;
String de = doc["dimmingEnabled"].as<String>();
dimmingEnabled = (de == "true" || de == "on" || de == "1");
@@ -253,10 +258,6 @@ void loadConfig() {
else
tempSymbol = '[';
if (doc.containsKey("showWeatherDescription"))
showWeatherDescription = doc["showWeatherDescription"];
else
showWeatherDescription = false;
// --- COUNTDOWN CONFIG LOADING ---
if (doc.containsKey("countdown")) {
@@ -408,19 +409,39 @@ void connectWiFi() {
// Time / NTP Functions
// -----------------------------------------------------------------------------
void setupTime() {
// sntp_stop();
if (!isAPMode) {
Serial.println(F("[TIME] Starting NTP sync"));
}
configTime(0, 0, ntpServer1, ntpServer2);
setenv("TZ", ianaToPosix(timeZone), 1);
tzset();
ntpState = NTP_SYNCING;
ntpStartTime = millis();
ntpRetryCount = 0;
ntpSyncSuccessful = false;
bool serverOk = false;
IPAddress resolvedIP;
// Try first server if it's not empty
if (strlen(ntpServer1) > 0 && WiFi.hostByName(ntpServer1, resolvedIP) == 1) {
serverOk = true;
}
// Try second server if first failed
else if (strlen(ntpServer2) > 0 && WiFi.hostByName(ntpServer2, resolvedIP) == 1) {
serverOk = true;
}
if (serverOk) {
configTime(0, 0, ntpServer1, ntpServer2); // safe to call now
setenv("TZ", ianaToPosix(timeZone), 1);
tzset();
ntpState = NTP_SYNCING;
ntpStartTime = millis();
ntpRetryCount = 0;
ntpSyncSuccessful = false;
} else {
Serial.println(F("[TIME] NTP server lookup failed — skipping sync"));
ntpSyncSuccessful = false;
ntpState = NTP_IDLE; // or custom NTP_ERROR state
// Trigger your error display here if desired
}
}
// -----------------------------------------------------------------------------
// Utility
// -----------------------------------------------------------------------------
@@ -454,6 +475,8 @@ void printConfigToSerial() {
Serial.println(twelveHourToggle ? "Yes" : "No");
Serial.print(F("Show Day of the Week: "));
Serial.println(showDayOfWeek ? "Yes" : "No");
Serial.print(F("Show Date: "));
Serial.println(showDate ? "Yes" : "No");
Serial.print(F("Show Weather Description: "));
Serial.println(showWeatherDescription ? "Yes" : "No");
Serial.print(F("Show Humidity: "));
@@ -550,6 +573,7 @@ void setupWebServer() {
else if (n == "flipDisplay") doc[n] = (v == "true" || v == "on" || v == "1");
else if (n == "twelveHourToggle") doc[n] = (v == "true" || v == "on" || v == "1");
else if (n == "showDayOfWeek") doc[n] = (v == "true" || v == "on" || v == "1");
else if (n == "showDate") doc[n] = (v == "true" || v == "on" || v == "1");
else if (n == "showHumidity") doc[n] = (v == "true" || v == "on" || v == "1");
else if (n == "colonBlinkEnabled") doc[n] = (v == "true" || v == "on" || v == "1");
else if (n == "dimStartHour") doc[n] = v.toInt();
@@ -779,6 +803,17 @@ void setupWebServer() {
request->send(200, "application/json", "{\"ok\":true}");
});
server.on("/set_showdate", HTTP_POST, [](AsyncWebServerRequest *request) {
bool showDateVal = false;
if (request->hasParam("value", true)) {
String v = request->getParam("value", true)->value();
showDateVal = (v == "1" || v == "true" || v == "on");
}
showDate = showDateVal;
Serial.printf("[WEBSERVER] Set showDate to %d\n", showDate);
request->send(200, "application/json", "{\"ok\":true}");
});
server.on("/set_humidity", HTTP_POST, [](AsyncWebServerRequest *request) {
bool showHumidityNow = false;
if (request->hasParam("value", true)) {
@@ -1249,8 +1284,11 @@ void advanceDisplayMode() {
String ntpField = String(ntpServer2);
bool nightscoutConfigured = ntpField.startsWith("https://");
if (displayMode == 0) { // Clock -> ...
if (weatherAvailable && (strlen(openWeatherApiKey) == 32) && (strlen(openWeatherCity) > 0) && (strlen(openWeatherCountry) > 0)) {
if (displayMode == 0) { // Clock
if (showDate) {
displayMode = 5; // Date mode right after Clock
Serial.println(F("[DISPLAY] Switching to display mode: DATE (from Clock)"));
} else if (weatherAvailable && (strlen(openWeatherApiKey) == 32) && (strlen(openWeatherCity) > 0) && (strlen(openWeatherCountry) > 0)) {
displayMode = 1;
Serial.println(F("[DISPLAY] Switching to display mode: WEATHER (from Clock)"));
} else if (countdownEnabled && !countdownFinished && ntpSyncSuccessful) {
@@ -1263,7 +1301,21 @@ void advanceDisplayMode() {
displayMode = 0;
Serial.println(F("[DISPLAY] Staying in CLOCK (from Clock)"));
}
} else if (displayMode == 1) { // Weather -> ...
} else if (displayMode == 5) { // Date mode
if (weatherAvailable && (strlen(openWeatherApiKey) == 32) && (strlen(openWeatherCity) > 0) && (strlen(openWeatherCountry) > 0)) {
displayMode = 1;
Serial.println(F("[DISPLAY] Switching to display mode: WEATHER (from Date)"));
} else if (countdownEnabled && !countdownFinished && ntpSyncSuccessful) {
displayMode = 3;
Serial.println(F("[DISPLAY] Switching to display mode: COUNTDOWN (from Date, weather skipped)"));
} else if (nightscoutConfigured) {
displayMode = 4;
Serial.println(F("[DISPLAY] Switching to display mode: NIGHTSCOUT (from Date, weather & countdown skipped)"));
} else {
displayMode = 0;
Serial.println(F("[DISPLAY] Switching to display mode: CLOCK (from Date)"));
}
} else if (displayMode == 1) { // Weather
if (showWeatherDescription && weatherAvailable && weatherDescription.length() > 0) {
displayMode = 2;
Serial.println(F("[DISPLAY] Switching to display mode: DESCRIPTION (from Weather)"));
@@ -1277,7 +1329,7 @@ void advanceDisplayMode() {
displayMode = 0;
Serial.println(F("[DISPLAY] Switching to display mode: CLOCK (from Weather)"));
}
} else if (displayMode == 2) { // Weather Description -> ...
} else if (displayMode == 2) { // Weather Description
if (countdownEnabled && !countdownFinished && ntpSyncSuccessful) {
displayMode = 3;
Serial.println(F("[DISPLAY] Switching to display mode: COUNTDOWN (from Description)"));
@@ -2042,5 +2094,93 @@ void loop() {
return;
}
}
//DATE Display Mode
else if (displayMode == 5 && showDate) {
// --- VALID DATE CHECK ---
if (timeinfo.tm_year < 120 || timeinfo.tm_mday <= 0 || timeinfo.tm_mon < 0 || timeinfo.tm_mon > 11) {
advanceDisplayMode();
return; // skip drawing
}
// -------------------------
String dateString;
// Get localized month names
const char *const *months = getMonthsOfYear(language);
String monthAbbr = String(months[timeinfo.tm_mon]).substring(0, 5);
monthAbbr.toLowerCase();
// Add spaces between day digits
String dayString = String(timeinfo.tm_mday);
String spacedDay = "";
for (size_t i = 0; i < dayString.length(); i++) {
spacedDay += dayString[i];
if (i < dayString.length() - 1) spacedDay += " ";
}
// Function to check if day should come first for given language
auto isDayFirst = [](const String &lang) {
// Languages with DD-MM order
const char *dayFirstLangs[] = {
"af", // Afrikaans
"cs", // Czech
"da", // Danish
"de", // German
"eo", // Esperanto
"es", // Spanish
"et", // Estonian
"fi", // Finnish
"fr", // French
"hr", // Croatian
"hu", // Hungarian
"it", // Italian
"lt", // Lithuanian
"lv", // Latvian
"nl", // Dutch
"no", // Norwegian
"pl", // Polish
"pt", // Portuguese
"ro", // Romanian
"sk", // Slovak
"sl", // Slovenian
"sr", // Serbian
"sv", // Swedish
"sw", // Swahili
"tr" // Turkish
};
for (auto lf : dayFirstLangs) {
if (lang.equalsIgnoreCase(lf)) {
return true;
}
}
return false;
};
String langForDate = String(language);
if (langForDate == "ja") {
// Japanese: month number (spaced digits) + day + symbol
String spacedMonth = "";
String monthNum = String(timeinfo.tm_mon + 1);
dateString = monthAbbr + " " + spacedDay + " ±";
} else {
if (isDayFirst(language)) {
dateString = spacedDay + " " + monthAbbr;
} else {
dateString = monthAbbr + " " + spacedDay;
}
}
P.setTextAlignment(PA_CENTER);
P.setCharSpacing(0);
P.print(dateString);
if (millis() - lastSwitch > weatherDuration) {
advanceDisplayMode();
}
}
yield();
}

View File

@@ -14,6 +14,7 @@
"ntpServer2": "time.nist.gov",
"twelveHourToggle": false,
"showDayOfWeek": true,
"showDate": false,
"showHumidity": false,
"colonBlinkEnabled": true,
"language": "en",

View File

@@ -322,7 +322,7 @@ textarea::placeholder {
<button type="button" class="primary-button" id="geo-button" onclick="getLocation()" style="margin-top: 1rem;">Get My Location</button>
<div class="small">
<strong>Location format examples:</strong> City, Country Code - Osaka, JP | ZIP,Country Code - 94040, US | Latitude, Longitude - 34.6937, 135.5023
<strong>Location format examples:</strong> City, Country Code - Osaka, JP | ZIP, Country Code - 94040, US | Latitude, Longitude - 34.6937, 135.5023
</div>
<div class="geo-note" style="display: none;">
<br>
@@ -495,6 +495,14 @@ textarea::placeholder {
</span>
</label>
<label style="display: flex; align-items: center; margin-top: 1.75rem; justify-content: space-between;">
<span style="margin-right: 0.5em;">Show Date:</span>
<span class="toggle-switch">
<input type="checkbox" id="showDate" name="showDate" onchange="setShowDate(this.checked)">
<span class="toggle-slider"></span>
</span>
</label>
<label style="display: flex; align-items: center; margin-top: 1.75rem; justify-content: space-between;">
<span style="margin-right: 0.5em;">Display 12-hour Clock:</span>
<span class="toggle-switch">
@@ -655,6 +663,7 @@ window.onload = function () {
document.getElementById('ntpServer2').value = data.ntpServer2 || "";
document.getElementById('twelveHourToggle').checked = !!data.twelveHourToggle;
document.getElementById('showDayOfWeek').checked = !!data.showDayOfWeek;
document.getElementById('showDate').checked = !!data.showDate;
document.getElementById('showHumidity').checked = !!data.showHumidity;
document.getElementById('colonBlinkEnabled').checked = !!data.colonBlinkEnabled;
document.getElementById('showWeatherDescription').checked = !!data.showWeatherDescription;
@@ -796,6 +805,7 @@ async function submitConfig(event) {
formData.set('flipDisplay', document.getElementById('flipDisplay').checked ? 'on' : '');
formData.set('twelveHourToggle', document.getElementById('twelveHourToggle').checked ? 'on' : '');
formData.set('showDayOfWeek', document.getElementById('showDayOfWeek').checked ? 'on' : '');
formData.set('showDate', document.getElementById('showDate').checked ? 'on' : '');
formData.set('showHumidity', document.getElementById('showHumidity').checked ? 'on' : '');
formData.set('colonBlinkEnabled', document.getElementById('colonBlinkEnabled').checked ? 'on' : '');
@@ -1098,6 +1108,14 @@ function setShowDayOfWeek(val) {
});
}
function setShowDate(val) {
fetch('/set_showdate', {
method: 'POST',
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "value=" + (val ? 1 : 0)
});
}
function setColonBlink(val) {
fetch('/set_colon_blink', {
method: 'POST',

View File

@@ -0,0 +1,50 @@
#ifndef MONTHS_LOOKUP_H
#define MONTHS_LOOKUP_H
typedef struct {
const char* lang;
const char* months[12]; // Jan to Dec
} MonthsMapping;
const MonthsMapping months_mappings[] = {
{ "af", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&e&i", "j&u&n", "j&u&l", "a&u&g", "s&e&p", "o&k&t", "n&o&v", "d&e&s" } }, // Afrikaans
{ "cs", { "l&e&d", "u&n&o", "b&r&e", "d&u&b", "k&v&e", "c&e&r", "c&v&c", "s&r&p", "z&a&r", "r&i&j", "l&i&s", "p&r&o" } }, // Czech
{ "da", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&a&j", "j&u&n", "j&u&l", "a&u&g", "s&e&p", "o&k&t", "n&o&v", "d&e&c" } }, // Danish
{ "de", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&a&i", "j&u&n", "j&u&l", "a&u&g", "s&e&p", "o&k&t", "n&o&v", "d&e&z" } }, // German
{ "en", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&a&y", "j&u&n", "j&u&l", "a&u&g", "s&e&p", "o&c&t", "n&o&v", "d&e&c" } }, // English
{ "eo", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&a&j", "j&u&n", "j&u&l", "a&u&g", "s&e&p", "o&k&t", "n&o&v", "d&e&c" } }, // Esperanto
{ "es", { "e&n&e", "f&e&b", "m&a&r", "a&b&r", "m&a&y", "j&u&n", "j&u&l", "a&g&o", "s&e&p", "o&c&t", "n&o&v", "d&i&c" } }, // Spanish
{ "et", { "j&a&n", "v&e&b", "m&a&r", "a&p&r", "m&a&i", "j&u&n", "j&u&l", "a&u&g", "s&e&p", "o&k&t", "n&o&v", "d&e&t" } }, // Estonian
{ "fi", { "t&a&m", "h&e&l", "m&a&a", "h&u&h", "t&o&u", "k&e&s", "h&e&i", "e&l&o", "s&y&y", "l&o&k", "m&a&r", "j&o&u" } }, // Finnish
{ "fr", { "j&a&n", "f&e&v", "m&a&r", "a&v&r", "m&a&i", "j&u&n", "j&u&l", "a&o&u", "s&e&p", "o&c&t", "n&o&v", "d&e&c" } }, // French
{ "hr", { "s&i&j", "v&e&l", "o&z&u", "t&r&a", "s&v&i", "l&i&p", "s&r&p", "k&o&l", "r&u&j", "l&i&s", "s&t&u", "p&r&o" } }, // Croatian
{ "hu", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&a&j", "j&u&n", "j&u&l", "a&u&g", "s&z&e", "o&k&t", "n&o&v", "d&e&c" } }, // Hungarian
{ "it", { "g&e&n", "f&e&b", "m&a&r", "a&p&r", "m&a&g", "g&i&u", "l&u&g", "a&g&o", "s&e&t", "o&t&t", "n&o&v", "d&i&c" } }, // Italian
{ "ja", { "1 ²", "2 ²", "3 ²", "4 ²", "5 ²", "6 ²", "7 ²", "8 ²", "9 ²", "10 ²", "11 ²", "12 ²" } }, // Japanese
{ "lt", { "s&a&u", "v&a&s", "k&o&v", "b&a&l", "g&e&g", "b&i&r", "l&i&e", "r&u&g", "s&w&e", "s&p&a", "l&a&p", "g&r&u" } }, // Lithuanian
{ "lv", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&a&i", "j&u&n", "j&u&l", "a&u&g", "s&e&p", "o&k&t", "n&o&v", "d&e&c" } }, // Latvian
{ "nl", { "j&a&n", "f&e&b", "m&a&a", "a&p&r", "m&e&i", "j&u&n", "j&u&l", "a&u&g", "s&e&p", "o&k&t", "n&o&v", "d&e&c" } }, // Dutch
{ "no", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&a&i", "j&u&n", "j&u&l", "a&u&g", "s&e&p", "o&k&t", "n&o&v", "d&e&s" } }, // Norwegian
{ "pl", { "s&t&y", "l&u&t", "m&a&r", "k&w&i", "m&a&j", "c&z&e", "l&i&p", "s&i&e", "w&r&z", "p&a&z", "l&i&s", "g&r&u" } }, // Polish
{ "pt", { "j&a&n", "f&e&v", "m&a&r", "a&b&r", "m&a&i", "j&u&n", "j&u&l", "a&g&o", "s&e&t", "o&u&t", "n&o&v", "d&e&z" } }, // Portuguese
{ "ro", { "i&a&n", "f&e&b", "m&a&r", "a&p&r", "m&a&i", "i&u&n", "i&u&l", "a&u&g", "s&e&p", "o&c&t", "n&o&v", "d&e&c" } }, // Romanian
{ "sk", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&a&j", "j&u&n", "j&u&l", "a&u&g", "s&e&p", "o&k&t", "n&o&v", "d&e&c" } }, // Slovak
{ "sl", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&a&j", "j&u&n", "j&u&l", "a&v&g", "s&e&p", "o&k&t", "n&o&v", "d&e&c" } }, // Slovenian
{ "sr", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&a&j", "j&u&n", "j&u&l", "a&v&g", "s&e&p", "o&k&t", "n&o&v", "d&e&c" } }, // Serbian
{ "sv", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&a&j", "j&u&n", "j&u&l", "a&u&g", "s&e&p", "o&k&t", "n&o&v", "d&e&c" } }, // Swedish
{ "sw", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&e&i", "j&u&n", "j&u&l", "a&g&o", "s&e&p", "o&k&t", "n&o&v", "d&e&s" } }, // Swahili
{ "tr", { "o&c&a", "s&u&b", "m&a&r", "n&i&s", "m&a&y", "h&a&z", "t&e&m", "a&g&u", "e&y&l", "e&k&i", "k&a&s", "a&r&a" } } // Turkish
};
#define MONTHS_MAPPINGS_COUNT (sizeof(months_mappings)/sizeof(months_mappings[0]))
inline const char* const* getMonthsOfYear(const char* lang) {
for (size_t i = 0; i < MONTHS_MAPPINGS_COUNT; i++) {
if (strcmp(lang, months_mappings[i].lang) == 0)
return months_mappings[i].months;
}
// fallback to English if not found
return months_mappings[4].months; // "en" is index 4
}
#endif // MONTHS_LOOKUP_H

View File

@@ -13,9 +13,10 @@
#include <time.h>
#include <WiFiClientSecure.h>
#include "mfactoryfont.h" // Custom font
#include "tz_lookup.h" // Timezone lookup, do not duplicate mapping here!
#include "days_lookup.h" // Languages for the Days of the Week
#include "mfactoryfont.h" // Custom font
#include "tz_lookup.h" // Timezone lookup, do not duplicate mapping here!
#include "days_lookup.h" // Languages for the Days of the Week
#include "months_lookup.h" // Languages for the Months of the Year
#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
#define MAX_DEVICES 4
@@ -49,6 +50,7 @@ int brightness = 7;
bool flipDisplay = false;
bool twelveHourToggle = false;
bool showDayOfWeek = true;
bool showDate = false;
bool showHumidity = false;
bool colonBlinkEnabled = true;
char ntpServer1[64] = "pool.ntp.org";
@@ -168,6 +170,7 @@ void loadConfig() {
doc[F("flipDisplay")] = flipDisplay;
doc[F("twelveHourToggle")] = twelveHourToggle;
doc[F("showDayOfWeek")] = showDayOfWeek;
doc[F("showDate")] = false;
doc[F("showHumidity")] = showHumidity;
doc[F("colonBlinkEnabled")] = colonBlinkEnabled;
doc[F("ntpServer1")] = ntpServer1;
@@ -233,8 +236,10 @@ void loadConfig() {
flipDisplay = doc["flipDisplay"] | false;
twelveHourToggle = doc["twelveHourToggle"] | false;
showDayOfWeek = doc["showDayOfWeek"] | true;
showDate = doc["showDate"] | false;
showHumidity = doc["showHumidity"] | false;
colonBlinkEnabled = doc.containsKey("colonBlinkEnabled") ? doc["colonBlinkEnabled"].as<bool>() : true;
showWeatherDescription = doc["showWeatherDescription"] | false;
String de = doc["dimmingEnabled"].as<String>();
dimmingEnabled = (de == "true" || de == "on" || de == "1");
@@ -253,10 +258,6 @@ void loadConfig() {
else
tempSymbol = '[';
if (doc.containsKey("showWeatherDescription"))
showWeatherDescription = doc["showWeatherDescription"];
else
showWeatherDescription = false;
// --- COUNTDOWN CONFIG LOADING ---
if (doc.containsKey("countdown")) {
@@ -412,13 +413,33 @@ void setupTime() {
if (!isAPMode) {
Serial.println(F("[TIME] Starting NTP sync"));
}
configTime(0, 0, ntpServer1, ntpServer2);
setenv("TZ", ianaToPosix(timeZone), 1);
tzset();
ntpState = NTP_SYNCING;
ntpStartTime = millis();
ntpRetryCount = 0;
ntpSyncSuccessful = false;
bool serverOk = false;
IPAddress resolvedIP;
// Try first server if it's not empty
if (strlen(ntpServer1) > 0 && WiFi.hostByName(ntpServer1, resolvedIP) == 1) {
serverOk = true;
}
// Try second server if first failed
else if (strlen(ntpServer2) > 0 && WiFi.hostByName(ntpServer2, resolvedIP) == 1) {
serverOk = true;
}
if (serverOk) {
configTime(0, 0, ntpServer1, ntpServer2); // safe to call now
setenv("TZ", ianaToPosix(timeZone), 1);
tzset();
ntpState = NTP_SYNCING;
ntpStartTime = millis();
ntpRetryCount = 0;
ntpSyncSuccessful = false;
} else {
Serial.println(F("[TIME] NTP server lookup failed — skipping sync"));
ntpSyncSuccessful = false;
ntpState = NTP_IDLE; // or custom NTP_ERROR state
// Trigger your error display here if desired
}
}
// -----------------------------------------------------------------------------
@@ -454,6 +475,8 @@ void printConfigToSerial() {
Serial.println(twelveHourToggle ? "Yes" : "No");
Serial.print(F("Show Day of the Week: "));
Serial.println(showDayOfWeek ? "Yes" : "No");
Serial.print(F("Show Date: "));
Serial.println(showDate ? "Yes" : "No");
Serial.print(F("Show Weather Description: "));
Serial.println(showWeatherDescription ? "Yes" : "No");
Serial.print(F("Show Humidity: "));
@@ -550,6 +573,7 @@ void setupWebServer() {
else if (n == "flipDisplay") doc[n] = (v == "true" || v == "on" || v == "1");
else if (n == "twelveHourToggle") doc[n] = (v == "true" || v == "on" || v == "1");
else if (n == "showDayOfWeek") doc[n] = (v == "true" || v == "on" || v == "1");
else if (n == "showDate") doc[n] = (v == "true" || v == "on" || v == "1");
else if (n == "showHumidity") doc[n] = (v == "true" || v == "on" || v == "1");
else if (n == "colonBlinkEnabled") doc[n] = (v == "true" || v == "on" || v == "1");
else if (n == "dimStartHour") doc[n] = v.toInt();
@@ -779,6 +803,17 @@ void setupWebServer() {
request->send(200, "application/json", "{\"ok\":true}");
});
server.on("/set_showdate", HTTP_POST, [](AsyncWebServerRequest *request) {
bool showDateVal = false;
if (request->hasParam("value", true)) {
String v = request->getParam("value", true)->value();
showDateVal = (v == "1" || v == "true" || v == "on");
}
showDate = showDateVal;
Serial.printf("[WEBSERVER] Set showDate to %d\n", showDate);
request->send(200, "application/json", "{\"ok\":true}");
});
server.on("/set_humidity", HTTP_POST, [](AsyncWebServerRequest *request) {
bool showHumidityNow = false;
if (request->hasParam("value", true)) {
@@ -1188,7 +1223,8 @@ DisplayMode key:
0: Clock
1: Weather
2: Weather Description
3: Countdown (NEW)
3: Countdown
4: Nightscout
*/
void setup() {
@@ -1241,13 +1277,77 @@ void setup() {
// void advanceDisplayMode() {
// int oldMode = displayMode;
// String ntpField = String(ntpServer2);
// bool nightscoutConfigured = ntpField.startsWith("https://");
// if (displayMode == 0) { // Clock -> ...
// if (weatherAvailable && (strlen(openWeatherApiKey) == 32) && (strlen(openWeatherCity) > 0) && (strlen(openWeatherCountry) > 0)) {
// displayMode = 1;
// Serial.println(F("[DISPLAY] Switching to display mode: WEATHER (from Clock)"));
// } else if (countdownEnabled && !countdownFinished && ntpSyncSuccessful) {
// displayMode = 3;
// Serial.println(F("[DISPLAY] Switching to display mode: COUNTDOWN (from Clock, weather skipped)"));
// } else if (nightscoutConfigured) {
// displayMode = 4; // Clock -> Nightscout (if weather & countdown are skipped)
// Serial.println(F("[DISPLAY] Switching to display mode: NIGHTSCOUT (from Clock, weather & countdown skipped)"));
// } else {
// displayMode = 0;
// Serial.println(F("[DISPLAY] Staying in CLOCK (from Clock)"));
// }
// } else if (displayMode == 1) { // Weather -> ...
// if (showWeatherDescription && weatherAvailable && weatherDescription.length() > 0) {
// displayMode = 2;
// Serial.println(F("[DISPLAY] Switching to display mode: DESCRIPTION (from Weather)"));
// } else if (countdownEnabled && !countdownFinished && ntpSyncSuccessful) {
// displayMode = 3;
// Serial.println(F("[DISPLAY] Switching to display mode: COUNTDOWN (from Weather)"));
// } else if (nightscoutConfigured) {
// displayMode = 4; // Weather -> Nightscout (if description & countdown are skipped)
// Serial.println(F("[DISPLAY] Switching to display mode: NIGHTSCOUT (from Weather, description & countdown skipped)"));
// } else {
// displayMode = 0;
// Serial.println(F("[DISPLAY] Switching to display mode: CLOCK (from Weather)"));
// }
// } else if (displayMode == 2) { // Weather Description -> ...
// if (countdownEnabled && !countdownFinished && ntpSyncSuccessful) {
// displayMode = 3;
// Serial.println(F("[DISPLAY] Switching to display mode: COUNTDOWN (from Description)"));
// } else if (nightscoutConfigured) {
// displayMode = 4; // Description -> Nightscout (if countdown is skipped)
// Serial.println(F("[DISPLAY] Switching to display mode: NIGHTSCOUT (from Description, countdown skipped)"));
// } else {
// displayMode = 0;
// Serial.println(F("[DISPLAY] Switching to display mode: CLOCK (from Description)"));
// }
// } else if (displayMode == 3) { // Countdown -> Nightscout
// if (nightscoutConfigured) {
// displayMode = 4;
// Serial.println(F("[DISPLAY] Switching to display mode: NIGHTSCOUT (from Countdown)"));
// } else {
// displayMode = 0;
// Serial.println(F("[DISPLAY] Switching to display mode: CLOCK (from Countdown)"));
// }
// } else if (displayMode == 4) { // Nightscout -> Clock
// displayMode = 0;
// Serial.println(F("[DISPLAY] Switching to display mode: CLOCK (from Nightscout)"));
// }
// // --- Common cleanup/reset logic remains the same ---
// lastSwitch = millis();
// }
void advanceDisplayMode() {
int oldMode = displayMode;
String ntpField = String(ntpServer2);
bool nightscoutConfigured = ntpField.startsWith("https://");
if (displayMode == 0) { // Clock -> ...
if (weatherAvailable && (strlen(openWeatherApiKey) == 32) && (strlen(openWeatherCity) > 0) && (strlen(openWeatherCountry) > 0)) {
if (showDate) {
displayMode = 5; // Date mode right after Clock
Serial.println(F("[DISPLAY] Switching to display mode: DATE (from Clock)"));
} else if (weatherAvailable && (strlen(openWeatherApiKey) == 32) && (strlen(openWeatherCity) > 0) && (strlen(openWeatherCountry) > 0)) {
displayMode = 1;
Serial.println(F("[DISPLAY] Switching to display mode: WEATHER (from Clock)"));
} else if (countdownEnabled && !countdownFinished && ntpSyncSuccessful) {
@@ -1260,6 +1360,20 @@ void advanceDisplayMode() {
displayMode = 0;
Serial.println(F("[DISPLAY] Staying in CLOCK (from Clock)"));
}
} else if (displayMode == 5) { // Date mode
if (weatherAvailable && (strlen(openWeatherApiKey) == 32) && (strlen(openWeatherCity) > 0) && (strlen(openWeatherCountry) > 0)) {
displayMode = 1;
Serial.println(F("[DISPLAY] Switching to display mode: WEATHER (from Date)"));
} else if (countdownEnabled && !countdownFinished && ntpSyncSuccessful) {
displayMode = 3;
Serial.println(F("[DISPLAY] Switching to display mode: COUNTDOWN (from Date, weather skipped)"));
} else if (nightscoutConfigured) {
displayMode = 4;
Serial.println(F("[DISPLAY] Switching to display mode: NIGHTSCOUT (from Date, weather & countdown skipped)"));
} else {
displayMode = 0;
Serial.println(F("[DISPLAY] Switching to display mode: CLOCK (from Date)"));
}
} else if (displayMode == 1) { // Weather -> ...
if (showWeatherDescription && weatherAvailable && weatherDescription.length() > 0) {
displayMode = 2;
@@ -2036,5 +2150,92 @@ void loop() {
return;
}
}
//DATE Display Mode
else if (displayMode == 5 && showDate) {
// --- VALID DATE CHECK ---
if (timeinfo.tm_year < 120 || timeinfo.tm_mday <= 0 || timeinfo.tm_mon < 0 || timeinfo.tm_mon > 11) {
advanceDisplayMode();
return; // skip drawing
}
// -------------------------
String dateString;
// Get localized month names
const char *const *months = getMonthsOfYear(language);
String monthAbbr = String(months[timeinfo.tm_mon]).substring(0, 5);
monthAbbr.toLowerCase();
// Add spaces between day digits
String dayString = String(timeinfo.tm_mday);
String spacedDay = "";
for (size_t i = 0; i < dayString.length(); i++) {
spacedDay += dayString[i];
if (i < dayString.length() - 1) spacedDay += " ";
}
// Function to check if day should come first for given language
auto isDayFirst = [](const String &lang) {
// Languages with DD-MM order
const char *dayFirstLangs[] = {
"af", // Afrikaans
"cs", // Czech
"da", // Danish
"de", // German
"eo", // Esperanto
"es", // Spanish
"et", // Estonian
"fi", // Finnish
"fr", // French
"hr", // Croatian
"hu", // Hungarian
"it", // Italian
"lt", // Lithuanian
"lv", // Latvian
"nl", // Dutch
"no", // Norwegian
"pl", // Polish
"pt", // Portuguese
"ro", // Romanian
"sk", // Slovak
"sl", // Slovenian
"sr", // Serbian
"sv", // Swedish
"sw", // Swahili
"tr" // Turkish
};
for (auto lf : dayFirstLangs) {
if (lang.equalsIgnoreCase(lf)) {
return true;
}
}
return false;
};
String langForDate = String(language);
if (langForDate == "ja") {
// Japanese: month number (spaced digits) + day + symbol
String spacedMonth = "";
String monthNum = String(timeinfo.tm_mon + 1);
dateString = monthAbbr + " " + spacedDay + " ±";
} else {
if (isDayFirst(language)) {
dateString = spacedDay + " " + monthAbbr;
} else {
dateString = monthAbbr + " " + spacedDay;
}
}
P.setTextAlignment(PA_CENTER);
P.setCharSpacing(0);
P.print(dateString);
if (millis() - lastSwitch > weatherDuration) {
advanceDisplayMode();
}
}
yield();
}

View File

@@ -14,6 +14,7 @@
"ntpServer2": "time.nist.gov",
"twelveHourToggle": false,
"showDayOfWeek": true,
"showDate": false,
"showHumidity": false,
"colonBlinkEnabled": true,
"language": "en",

View File

@@ -322,7 +322,7 @@ textarea::placeholder {
<button type="button" class="primary-button" id="geo-button" onclick="getLocation()" style="margin-top: 1rem;">Get My Location</button>
<div class="small">
<strong>Location format examples:</strong> City, Country Code - Osaka, JP | ZIP,Country Code - 94040, US | Latitude, Longitude - 34.6937, 135.5023
<strong>Location format examples:</strong> City, Country Code - Osaka, JP | ZIP, Country Code - 94040, US | Latitude, Longitude - 34.6937, 135.5023
</div>
<div class="geo-note" style="display: none;">
<br>
@@ -495,6 +495,14 @@ textarea::placeholder {
</span>
</label>
<label style="display: flex; align-items: center; margin-top: 1.75rem; justify-content: space-between;">
<span style="margin-right: 0.5em;">Show Date:</span>
<span class="toggle-switch">
<input type="checkbox" id="showDate" name="showDate" onchange="setShowDate(this.checked)">
<span class="toggle-slider"></span>
</span>
</label>
<label style="display: flex; align-items: center; margin-top: 1.75rem; justify-content: space-between;">
<span style="margin-right: 0.5em;">Display 12-hour Clock:</span>
<span class="toggle-switch">
@@ -655,6 +663,7 @@ window.onload = function () {
document.getElementById('ntpServer2').value = data.ntpServer2 || "";
document.getElementById('twelveHourToggle').checked = !!data.twelveHourToggle;
document.getElementById('showDayOfWeek').checked = !!data.showDayOfWeek;
document.getElementById('showDate').checked = !!data.showDate;
document.getElementById('showHumidity').checked = !!data.showHumidity;
document.getElementById('colonBlinkEnabled').checked = !!data.colonBlinkEnabled;
document.getElementById('showWeatherDescription').checked = !!data.showWeatherDescription;
@@ -796,6 +805,7 @@ async function submitConfig(event) {
formData.set('flipDisplay', document.getElementById('flipDisplay').checked ? 'on' : '');
formData.set('twelveHourToggle', document.getElementById('twelveHourToggle').checked ? 'on' : '');
formData.set('showDayOfWeek', document.getElementById('showDayOfWeek').checked ? 'on' : '');
formData.set('showDate', document.getElementById('showDate').checked ? 'on' : '');
formData.set('showHumidity', document.getElementById('showHumidity').checked ? 'on' : '');
formData.set('colonBlinkEnabled', document.getElementById('colonBlinkEnabled').checked ? 'on' : '');
@@ -1098,6 +1108,14 @@ function setShowDayOfWeek(val) {
});
}
function setShowDate(val) {
fetch('/set_showdate', {
method: 'POST',
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "value=" + (val ? 1 : 0)
});
}
function setColonBlink(val) {
fetch('/set_colon_blink', {
method: 'POST',

View File

@@ -0,0 +1,50 @@
#ifndef MONTHS_LOOKUP_H
#define MONTHS_LOOKUP_H
typedef struct {
const char* lang;
const char* months[12]; // Jan to Dec
} MonthsMapping;
const MonthsMapping months_mappings[] = {
{ "af", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&e&i", "j&u&n", "j&u&l", "a&u&g", "s&e&p", "o&k&t", "n&o&v", "d&e&s" } }, // Afrikaans
{ "cs", { "l&e&d", "u&n&o", "b&r&e", "d&u&b", "k&v&e", "c&e&r", "c&v&c", "s&r&p", "z&a&r", "r&i&j", "l&i&s", "p&r&o" } }, // Czech
{ "da", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&a&j", "j&u&n", "j&u&l", "a&u&g", "s&e&p", "o&k&t", "n&o&v", "d&e&c" } }, // Danish
{ "de", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&a&i", "j&u&n", "j&u&l", "a&u&g", "s&e&p", "o&k&t", "n&o&v", "d&e&z" } }, // German
{ "en", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&a&y", "j&u&n", "j&u&l", "a&u&g", "s&e&p", "o&c&t", "n&o&v", "d&e&c" } }, // English
{ "eo", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&a&j", "j&u&n", "j&u&l", "a&u&g", "s&e&p", "o&k&t", "n&o&v", "d&e&c" } }, // Esperanto
{ "es", { "e&n&e", "f&e&b", "m&a&r", "a&b&r", "m&a&y", "j&u&n", "j&u&l", "a&g&o", "s&e&p", "o&c&t", "n&o&v", "d&i&c" } }, // Spanish
{ "et", { "j&a&n", "v&e&b", "m&a&r", "a&p&r", "m&a&i", "j&u&n", "j&u&l", "a&u&g", "s&e&p", "o&k&t", "n&o&v", "d&e&t" } }, // Estonian
{ "fi", { "t&a&m", "h&e&l", "m&a&a", "h&u&h", "t&o&u", "k&e&s", "h&e&i", "e&l&o", "s&y&y", "l&o&k", "m&a&r", "j&o&u" } }, // Finnish
{ "fr", { "j&a&n", "f&e&v", "m&a&r", "a&v&r", "m&a&i", "j&u&n", "j&u&l", "a&o&u", "s&e&p", "o&c&t", "n&o&v", "d&e&c" } }, // French
{ "hr", { "s&i&j", "v&e&l", "o&z&u", "t&r&a", "s&v&i", "l&i&p", "s&r&p", "k&o&l", "r&u&j", "l&i&s", "s&t&u", "p&r&o" } }, // Croatian
{ "hu", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&a&j", "j&u&n", "j&u&l", "a&u&g", "s&z&e", "o&k&t", "n&o&v", "d&e&c" } }, // Hungarian
{ "it", { "g&e&n", "f&e&b", "m&a&r", "a&p&r", "m&a&g", "g&i&u", "l&u&g", "a&g&o", "s&e&t", "o&t&t", "n&o&v", "d&i&c" } }, // Italian
{ "ja", { "1 ²", "2 ²", "3 ²", "4 ²", "5 ²", "6 ²", "7 ²", "8 ²", "9 ²", "10 ²", "11 ²", "12 ²" } }, // Japanese
{ "lt", { "s&a&u", "v&a&s", "k&o&v", "b&a&l", "g&e&g", "b&i&r", "l&i&e", "r&u&g", "s&w&e", "s&p&a", "l&a&p", "g&r&u" } }, // Lithuanian
{ "lv", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&a&i", "j&u&n", "j&u&l", "a&u&g", "s&e&p", "o&k&t", "n&o&v", "d&e&c" } }, // Latvian
{ "nl", { "j&a&n", "f&e&b", "m&a&a", "a&p&r", "m&e&i", "j&u&n", "j&u&l", "a&u&g", "s&e&p", "o&k&t", "n&o&v", "d&e&c" } }, // Dutch
{ "no", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&a&i", "j&u&n", "j&u&l", "a&u&g", "s&e&p", "o&k&t", "n&o&v", "d&e&s" } }, // Norwegian
{ "pl", { "s&t&y", "l&u&t", "m&a&r", "k&w&i", "m&a&j", "c&z&e", "l&i&p", "s&i&e", "w&r&z", "p&a&z", "l&i&s", "g&r&u" } }, // Polish
{ "pt", { "j&a&n", "f&e&v", "m&a&r", "a&b&r", "m&a&i", "j&u&n", "j&u&l", "a&g&o", "s&e&t", "o&u&t", "n&o&v", "d&e&z" } }, // Portuguese
{ "ro", { "i&a&n", "f&e&b", "m&a&r", "a&p&r", "m&a&i", "i&u&n", "i&u&l", "a&u&g", "s&e&p", "o&c&t", "n&o&v", "d&e&c" } }, // Romanian
{ "sk", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&a&j", "j&u&n", "j&u&l", "a&u&g", "s&e&p", "o&k&t", "n&o&v", "d&e&c" } }, // Slovak
{ "sl", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&a&j", "j&u&n", "j&u&l", "a&v&g", "s&e&p", "o&k&t", "n&o&v", "d&e&c" } }, // Slovenian
{ "sr", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&a&j", "j&u&n", "j&u&l", "a&v&g", "s&e&p", "o&k&t", "n&o&v", "d&e&c" } }, // Serbian
{ "sv", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&a&j", "j&u&n", "j&u&l", "a&u&g", "s&e&p", "o&k&t", "n&o&v", "d&e&c" } }, // Swedish
{ "sw", { "j&a&n", "f&e&b", "m&a&r", "a&p&r", "m&e&i", "j&u&n", "j&u&l", "a&g&o", "s&e&p", "o&k&t", "n&o&v", "d&e&s" } }, // Swahili
{ "tr", { "o&c&a", "s&u&b", "m&a&r", "n&i&s", "m&a&y", "h&a&z", "t&e&m", "a&g&u", "e&y&l", "e&k&i", "k&a&s", "a&r&a" } } // Turkish
};
#define MONTHS_MAPPINGS_COUNT (sizeof(months_mappings)/sizeof(months_mappings[0]))
inline const char* const* getMonthsOfYear(const char* lang) {
for (size_t i = 0; i < MONTHS_MAPPINGS_COUNT; i++) {
if (strcmp(lang, months_mappings[i].lang) == 0)
return months_mappings[i].months;
}
// fallback to English if not found
return months_mappings[4].months; // "en" is index 4
}
#endif // MONTHS_LOOKUP_H