mirror of
https://github.com/mfactory-osaka/ESPTimeCast.git
synced 2026-02-19 11:54:56 -05:00
Added Home Assistant Support
Added Home Assistant Support. Bug fixes
This commit is contained in:
@@ -30,6 +30,7 @@ AsyncWebServer server(80);
|
||||
// --- Global Scroll Speed Settings ---
|
||||
const int GENERAL_SCROLL_SPEED = 85; // Default: Adjust this for Weather Description and Countdown Label (e.g., 50 for faster, 200 for slower)
|
||||
const int IP_SCROLL_SPEED = 115; // Default: Adjust this for the IP Address display (slower for readability)
|
||||
int messageScrollSpeed = 85; // default fallback
|
||||
|
||||
// --- Nightscout setting ---
|
||||
const unsigned int NIGHTSCOUT_IDLE_THRESHOLD_MIN = 10; // minutes before data is considered outdated
|
||||
@@ -59,6 +60,8 @@ bool showHumidity = false;
|
||||
bool colonBlinkEnabled = true;
|
||||
char ntpServer1[64] = "pool.ntp.org";
|
||||
char ntpServer2[256] = "time.nist.gov";
|
||||
char customMessage[121] = "";
|
||||
char lastPersistentMessage[128] = "";
|
||||
|
||||
// Dimming
|
||||
bool dimmingEnabled = false;
|
||||
@@ -101,6 +104,8 @@ bool shouldFetchWeatherNow = false;
|
||||
unsigned long lastSwitch = 0;
|
||||
unsigned long lastColonBlink = 0;
|
||||
int displayMode = 0; // 0: Clock, 1: Weather, 2: Weather Description, 3: Countdown
|
||||
int prevDisplayMode = -1;
|
||||
bool clockScrollDone = false;
|
||||
int currentHumidity = -1;
|
||||
bool ntpSyncSuccessful = false;
|
||||
|
||||
@@ -147,7 +152,11 @@ const unsigned long descriptionScrollPause = 300; // 300ms pause after scroll
|
||||
|
||||
// --- Safe WiFi credential and API getters ---
|
||||
const char *getSafeSsid() {
|
||||
return isAPMode ? "********" : ssid;
|
||||
if (isAPMode && strlen(ssid) == 0) {
|
||||
return "";
|
||||
} else {
|
||||
return isAPMode ? "********" : ssid;
|
||||
}
|
||||
}
|
||||
|
||||
const char *getSafePassword() {
|
||||
@@ -179,6 +188,7 @@ textEffect_t getEffectiveScrollDirection(textEffect_t desiredDirection, bool isF
|
||||
return desiredDirection;
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Configuration Load & Save
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -252,10 +262,12 @@ void loadConfig() {
|
||||
|
||||
strlcpy(ssid, doc["ssid"] | "", sizeof(ssid));
|
||||
strlcpy(password, doc["password"] | "", sizeof(password));
|
||||
strlcpy(openWeatherApiKey, doc["openWeatherApiKey"] | "", sizeof(openWeatherApiKey)); // Corrected typo here
|
||||
strlcpy(openWeatherApiKey, doc["openWeatherApiKey"] | "", sizeof(openWeatherApiKey));
|
||||
strlcpy(openWeatherCity, doc["openWeatherCity"] | "", sizeof(openWeatherCity));
|
||||
strlcpy(openWeatherCountry, doc["openWeatherCountry"] | "", sizeof(openWeatherCountry));
|
||||
strlcpy(weatherUnits, doc["weatherUnits"] | "metric", sizeof(weatherUnits));
|
||||
strlcpy(customMessage, doc["customMessage"] | "", sizeof(customMessage));
|
||||
strlcpy(lastPersistentMessage, customMessage, sizeof(lastPersistentMessage));
|
||||
clockDuration = doc["clockDuration"] | 10000;
|
||||
weatherDuration = doc["weatherDuration"] | 5000;
|
||||
strlcpy(timeZone, doc["timeZone"] | "Etc/UTC", sizeof(timeZone));
|
||||
@@ -325,7 +337,6 @@ void loadConfig() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// WiFi Setup
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -449,6 +460,7 @@ void connectWiFi() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void clearWiFiCredentialsInConfig() {
|
||||
DynamicJsonDocument doc(2048);
|
||||
|
||||
@@ -482,6 +494,7 @@ void clearWiFiCredentialsInConfig() {
|
||||
Serial.println(F("[SECURITY] Cleared WiFi credentials in config.json."));
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Time / NTP Functions
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -584,6 +597,8 @@ void printConfigToSerial() {
|
||||
Serial.println(countdownLabel);
|
||||
Serial.print(F("Dramatic Countdown Display: "));
|
||||
Serial.println(isDramaticCountdown ? "Yes" : "No");
|
||||
Serial.print(F("Custom Message: "));
|
||||
Serial.println(customMessage);
|
||||
Serial.print(F("Total Runtime: "));
|
||||
if (totalUptimeSeconds > 0) {
|
||||
Serial.println(formatUptime(totalUptimeSeconds));
|
||||
@@ -594,6 +609,7 @@ void printConfigToSerial() {
|
||||
Serial.println();
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Web Server and Captive Portal
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -793,6 +809,7 @@ void setupWebServer() {
|
||||
|
||||
Serial.println(F("[SAVE] Config verification successful."));
|
||||
DynamicJsonDocument okDoc(128);
|
||||
strlcpy(customMessage, doc["customMessage"] | "", sizeof(customMessage));
|
||||
okDoc[F("message")] = "Saved successfully. Rebooting...";
|
||||
String response;
|
||||
serializeJson(okDoc, response);
|
||||
@@ -1101,6 +1118,110 @@ void setupWebServer() {
|
||||
request->send(200, "application/json", "{\"ok\":true}");
|
||||
});
|
||||
|
||||
// --- Custom Message Endpoint ---
|
||||
server.on("/set_custom_message", HTTP_POST, [](AsyncWebServerRequest *request) {
|
||||
if (request->hasParam("message", true)) {
|
||||
String msg = request->getParam("message", true)->value();
|
||||
msg.trim();
|
||||
|
||||
String sourceHeader = request->header("X-Source");
|
||||
bool isFromUI = (sourceHeader == "UI");
|
||||
bool isFromHA = !isFromUI;
|
||||
|
||||
// --- Local speed variable (does not modify global GENERAL_SCROLL_SPEED) ---
|
||||
int localSpeed = GENERAL_SCROLL_SPEED; // Default for UI messages
|
||||
if (request->hasParam("speed", true)) {
|
||||
localSpeed = constrain(request->getParam("speed", true)->value().toInt(), 10, 200);
|
||||
Serial.printf("[MESSAGE] Custom Message scroll speed set to %d\n", localSpeed);
|
||||
}
|
||||
|
||||
// --- CLEAR MESSAGE ---
|
||||
if (msg.length() == 0) {
|
||||
if (isFromUI) {
|
||||
// Web UI clear: remove everything
|
||||
customMessage[0] = '\0';
|
||||
lastPersistentMessage[0] = '\0';
|
||||
displayMode = 0;
|
||||
Serial.println(F("[MESSAGE] All messages cleared by UI. Returning to normal mode."));
|
||||
request->send(200, "text/plain", "CLEARED (UI)");
|
||||
|
||||
// --- SAVE CLEAR STATE ---
|
||||
saveCustomMessageToConfig("");
|
||||
} else {
|
||||
// HA clear: remove only temporary message
|
||||
customMessage[0] = '\0';
|
||||
|
||||
if (strlen(lastPersistentMessage) > 0) {
|
||||
// Restore the last persistent message
|
||||
strncpy(customMessage, lastPersistentMessage, sizeof(customMessage));
|
||||
messageScrollSpeed = GENERAL_SCROLL_SPEED; // Use global speed for persistent
|
||||
Serial.printf("[MESSAGE] Temporary HA message cleared. Restored persistent message: '%s' (speed=%d)\n",
|
||||
customMessage, messageScrollSpeed);
|
||||
request->send(200, "text/plain", "CLEARED (HA temporary, persistent restored)");
|
||||
} else {
|
||||
displayMode = 0;
|
||||
Serial.println(F("[MESSAGE] Temporary HA message cleared. No persistent message to restore."));
|
||||
request->send(200, "text/plain", "CLEARED (HA temporary, no persistent)");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// --- SANITIZE MESSAGE ---
|
||||
msg.toUpperCase();
|
||||
String filtered = "";
|
||||
for (size_t i = 0; i < msg.length(); i++) {
|
||||
char c = msg[i];
|
||||
if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == ' ' || c == ':' || c == '!' || c == '\'' || c == '-' || c == '.' || c == ',' || c == '_' || c == '+' || c == '%' || c == '/' || c == '?') {
|
||||
filtered += c;
|
||||
}
|
||||
}
|
||||
|
||||
filtered.toCharArray(customMessage, sizeof(customMessage));
|
||||
|
||||
// --- STORE MESSAGE ---
|
||||
if (isFromHA) {
|
||||
// --- Preserve the current persistent message before overwriting ---
|
||||
char prevMessage[sizeof(customMessage)];
|
||||
strlcpy(prevMessage, customMessage, sizeof(prevMessage));
|
||||
|
||||
// --- Overwrite customMessage with new temporary HA message ---
|
||||
filtered.toCharArray(customMessage, sizeof(customMessage));
|
||||
messageScrollSpeed = localSpeed; // Use HA-specified scroll speed
|
||||
|
||||
// --- If no persistent message stored yet, keep the previous one ---
|
||||
if (strlen(lastPersistentMessage) == 0 && strlen(prevMessage) > 0) {
|
||||
strlcpy(lastPersistentMessage, prevMessage, sizeof(lastPersistentMessage));
|
||||
}
|
||||
|
||||
Serial.printf("[HA] Temporary HA message received: %s (persistent=%s)\n",
|
||||
customMessage, lastPersistentMessage);
|
||||
|
||||
} else {
|
||||
// --- UI-originated message: permanent ---
|
||||
filtered.toCharArray(customMessage, sizeof(customMessage));
|
||||
strlcpy(lastPersistentMessage, customMessage, sizeof(lastPersistentMessage));
|
||||
messageScrollSpeed = GENERAL_SCROLL_SPEED; // Always global for UI
|
||||
|
||||
Serial.printf("[UI] Persistent message stored: %s (speed=%d)\n",
|
||||
customMessage, messageScrollSpeed);
|
||||
|
||||
// --- Persist to config.json immediately ---
|
||||
saveCustomMessageToConfig(customMessage);
|
||||
}
|
||||
|
||||
// --- Activate display ---
|
||||
displayMode = 6;
|
||||
prevDisplayMode = 0;
|
||||
|
||||
String response = String(isFromHA ? "OK (HA message, speed=" : "OK (UI message, speed=") + String(localSpeed) + ")";
|
||||
request->send(200, "text/plain", response);
|
||||
} else {
|
||||
Serial.println(F("[MESSAGE] Error: missing 'message' parameter in request."));
|
||||
request->send(400, "text/plain", "Missing message parameter");
|
||||
}
|
||||
});
|
||||
|
||||
server.on("/uptime", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
if (!LittleFS.exists("/uptime.dat")) {
|
||||
request->send(200, "text/plain", "No uptime recorded yet.");
|
||||
@@ -1291,6 +1412,7 @@ void setupWebServer() {
|
||||
Serial.println(F("[WEBSERVER] Web server started"));
|
||||
}
|
||||
|
||||
|
||||
void handleCaptivePortal(AsyncWebServerRequest *request) {
|
||||
Serial.print(F("[WEBSERVER] Captive Portal triggered for URL: "));
|
||||
Serial.println(request->url());
|
||||
@@ -1307,6 +1429,7 @@ void handleCaptivePortal(AsyncWebServerRequest *request) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
String normalizeWeatherDescription(String str) {
|
||||
// Serbian Cyrillic → Latin
|
||||
str.replace("а", "a");
|
||||
@@ -1467,7 +1590,6 @@ bool isFiveDigitZip(const char *str) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Weather Fetching and API settings
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -1506,7 +1628,6 @@ String buildWeatherURL() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
void fetchWeather() {
|
||||
Serial.println(F("[WEATHER] Fetching weather data..."));
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
@@ -1603,6 +1724,7 @@ void fetchWeather() {
|
||||
http.end();
|
||||
}
|
||||
|
||||
|
||||
void loadUptime() {
|
||||
if (LittleFS.exists("/uptime.dat")) {
|
||||
File f = LittleFS.open("/uptime.dat", "r");
|
||||
@@ -1620,6 +1742,7 @@ void loadUptime() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void saveUptime() {
|
||||
// Add runtime since boot to total
|
||||
unsigned long runtimeSeconds = (millis() - bootMillis) / 1000;
|
||||
@@ -1639,6 +1762,41 @@ void saveUptime() {
|
||||
}
|
||||
}
|
||||
|
||||
void saveCustomMessageToConfig(const char *msg) {
|
||||
Serial.println(F("[CONFIG] Updating customMessage in config.json..."));
|
||||
|
||||
DynamicJsonDocument doc(2048);
|
||||
|
||||
// Load existing config.json (if present)
|
||||
File configFile = LittleFS.open("/config.json", "r");
|
||||
if (configFile) {
|
||||
DeserializationError err = deserializeJson(doc, configFile);
|
||||
configFile.close();
|
||||
if (err) {
|
||||
Serial.print(F("[CONFIG] Error reading existing config: "));
|
||||
Serial.println(err.f_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Update only customMessage
|
||||
doc["customMessage"] = msg;
|
||||
|
||||
// Safely write back to config.json
|
||||
if (LittleFS.exists("/config.json")) {
|
||||
LittleFS.rename("/config.json", "/config.bak");
|
||||
}
|
||||
|
||||
File f = LittleFS.open("/config.json", "w");
|
||||
if (!f) {
|
||||
Serial.println(F("[CONFIG] ERROR: Failed to open /config.json for writing"));
|
||||
return;
|
||||
}
|
||||
|
||||
size_t bytesWritten = serializeJson(doc, f);
|
||||
f.close();
|
||||
Serial.printf("[CONFIG] Saved customMessage='%s' (%u bytes written)\n", msg, bytesWritten);
|
||||
}
|
||||
|
||||
// Returns formatted uptime (for web UI or logs)
|
||||
String formatUptime(unsigned long seconds) {
|
||||
unsigned long days = seconds / 86400;
|
||||
@@ -1655,7 +1813,6 @@ String formatUptime(unsigned long seconds) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Main setup() and loop()
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -1664,9 +1821,11 @@ DisplayMode key:
|
||||
0: Clock
|
||||
1: Weather
|
||||
2: Weather Description
|
||||
3: Countdown (NEW)
|
||||
3: Countdown
|
||||
4: Nightscout
|
||||
5: Date
|
||||
6: Custom Message
|
||||
*/
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
@@ -1718,8 +1877,8 @@ void setup() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
void advanceDisplayMode() {
|
||||
prevDisplayMode = displayMode;
|
||||
int oldMode = displayMode;
|
||||
String ntpField = String(ntpServer2);
|
||||
bool nightscoutConfigured = ntpField.startsWith("https://");
|
||||
@@ -1788,18 +1947,26 @@ void advanceDisplayMode() {
|
||||
displayMode = 0;
|
||||
Serial.println(F("[DISPLAY] Switching to display mode: CLOCK (from Countdown)"));
|
||||
}
|
||||
} else if (displayMode == 4) { // Nightscout -> Clock
|
||||
} else if (displayMode == 4) { // Nightscout -> Custom Message
|
||||
displayMode = 6;
|
||||
Serial.println(F("[DISPLAY] Switching to display mode: CUSTOM MESSAGE (from Nightscout)"));
|
||||
} else if (displayMode == 6) { // Custom Message -> Clock
|
||||
displayMode = 0;
|
||||
Serial.println(F("[DISPLAY] Switching to display mode: CLOCK (from Nightscout)"));
|
||||
Serial.println(F("[DISPLAY] Switching to display mode: CLOCK (from Custom Message)"));
|
||||
}
|
||||
|
||||
// --- Common cleanup/reset logic remains the same ---
|
||||
if ((displayMode == 0) && strlen(customMessage) > 0 && oldMode != 6) {
|
||||
displayMode = 6;
|
||||
Serial.println(F("[DISPLAY] Custom Message display before returning to CLOCK"));
|
||||
}
|
||||
lastSwitch = millis();
|
||||
}
|
||||
|
||||
|
||||
void advanceDisplayModeSafe() {
|
||||
int attempts = 0;
|
||||
const int MAX_ATTEMPTS = 6; // Number of possible modes + 1
|
||||
const int MAX_ATTEMPTS = 7; // Number of possible modes + 1
|
||||
int startMode = displayMode;
|
||||
bool valid = false;
|
||||
do {
|
||||
@@ -1816,6 +1983,7 @@ void advanceDisplayModeSafe() {
|
||||
else if (displayMode == 2 && showWeatherDescription && weatherAvailable && weatherDescription.length() > 0) valid = true;
|
||||
else if (displayMode == 3 && countdownEnabled && !countdownFinished && ntpSyncSuccessful) valid = true;
|
||||
else if (displayMode == 4 && nightscoutConfigured) valid = true;
|
||||
else if (displayMode == 6 && strlen(customMessage) > 0) valid = true;
|
||||
|
||||
// If we've looped back to where we started, break to avoid infinite loop
|
||||
if (displayMode == startMode) break;
|
||||
@@ -1831,6 +1999,7 @@ void advanceDisplayModeSafe() {
|
||||
lastSwitch = millis();
|
||||
}
|
||||
|
||||
|
||||
//config save after countdown finishes
|
||||
bool saveCountdownConfig(bool enabled, time_t targetTimestamp, const String &label) {
|
||||
DynamicJsonDocument doc(2048);
|
||||
@@ -1894,7 +2063,6 @@ void loop() {
|
||||
const unsigned long fetchInterval = 300000; // 5 minutes
|
||||
|
||||
|
||||
|
||||
// AP Mode animation
|
||||
static unsigned long apAnimTimer = 0;
|
||||
static int apAnimFrame = 0;
|
||||
@@ -1977,7 +2145,6 @@ void loop() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --- IMMEDIATE COUNTDOWN FINISH TRIGGER ---
|
||||
if (countdownEnabled && !countdownFinished && ntpSyncSuccessful && countdownTargetTimestamp > 0 && now_time >= countdownTargetTimestamp) {
|
||||
countdownFinished = true;
|
||||
@@ -2011,7 +2178,6 @@ void loop() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --- BRIGHTNESS/OFF CHECK ---
|
||||
if (brightness == -1) {
|
||||
if (!displayOff) {
|
||||
@@ -2024,7 +2190,6 @@ void loop() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --- NTP State Machine ---
|
||||
switch (ntpState) {
|
||||
case NTP_IDLE: break;
|
||||
@@ -2097,7 +2262,6 @@ void loop() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --- MODIFIED WEATHER FETCHING LOGIC ---
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
if (!weatherFetchInitiated || shouldFetchWeatherNow || (millis() - lastFetch > fetchInterval)) {
|
||||
@@ -2177,11 +2341,6 @@ void loop() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Persistent variables (declare near top of file or loop)
|
||||
static int prevDisplayMode = -1;
|
||||
static bool clockScrollDone = false;
|
||||
|
||||
// --- CLOCK Display Mode ---
|
||||
if (displayMode == 0) {
|
||||
P.setCharSpacing(0);
|
||||
@@ -2231,6 +2390,8 @@ void loop() {
|
||||
shouldScrollIn = true; // first boot or other special modes
|
||||
} else if (prevDisplayMode == 2 && weatherDescription.length() > 8) {
|
||||
shouldScrollIn = true; // only scroll in if weather was scrolling
|
||||
} else if (prevDisplayMode == 6) {
|
||||
shouldScrollIn = true; // scroll in when coming from custom message
|
||||
}
|
||||
|
||||
if (shouldScrollIn && !clockScrollDone) {
|
||||
@@ -2259,10 +2420,6 @@ void loop() {
|
||||
}
|
||||
}
|
||||
|
||||
// --- update prevDisplayMode ---
|
||||
prevDisplayMode = displayMode;
|
||||
|
||||
|
||||
|
||||
// --- WEATHER Display Mode ---
|
||||
static bool weatherWasAvailable = false;
|
||||
@@ -2299,12 +2456,22 @@ void loop() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// --- WEATHER DESCRIPTION Display Mode ---
|
||||
if (displayMode == 2 && showWeatherDescription && weatherAvailable && weatherDescription.length() > 0) {
|
||||
String desc = weatherDescription;
|
||||
|
||||
// --- Check if humidity is actually visible ---
|
||||
bool humidityVisible = showHumidity && weatherAvailable && strlen(openWeatherApiKey) == 32 && strlen(openWeatherCity) > 0 && strlen(openWeatherCountry) > 0;
|
||||
|
||||
// --- Conditional padding ---
|
||||
bool addPadding = false;
|
||||
if (prevDisplayMode == 1 && humidityVisible) {
|
||||
addPadding = true;
|
||||
}
|
||||
if (addPadding) {
|
||||
desc = " " + desc; // 4-space padding before scrolling
|
||||
}
|
||||
|
||||
// prepare safe buffer
|
||||
static char descBuffer[128]; // large enough for OWM translations
|
||||
desc.toCharArray(descBuffer, sizeof(descBuffer));
|
||||
@@ -2617,10 +2784,17 @@ void loop() {
|
||||
}
|
||||
|
||||
String fullString = String(buf);
|
||||
bool addPadding = false;
|
||||
bool humidityVisible = showHumidity && weatherAvailable && strlen(openWeatherApiKey) == 32 && strlen(openWeatherCity) > 0 && strlen(openWeatherCountry) > 0;
|
||||
|
||||
// --- Add a leading space only if showDayOfWeek is true ---
|
||||
if (showDayOfWeek) {
|
||||
fullString = " " + fullString;
|
||||
// Padding logic
|
||||
if (prevDisplayMode == 0 && (showDayOfWeek || colonBlinkEnabled)) {
|
||||
addPadding = true;
|
||||
} else if (prevDisplayMode == 1 && humidityVisible) {
|
||||
addPadding = true;
|
||||
}
|
||||
if (addPadding) {
|
||||
fullString = " " + fullString; // 4 spaces
|
||||
}
|
||||
|
||||
// Display the full string and scroll it
|
||||
@@ -2898,6 +3072,53 @@ void loop() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Custom Message Display Mode (displayMode == 6) ---
|
||||
if (displayMode == 6) {
|
||||
if (strlen(customMessage) == 0) {
|
||||
advanceDisplayMode();
|
||||
yield();
|
||||
return;
|
||||
}
|
||||
|
||||
String msg = String(customMessage);
|
||||
|
||||
// Replace standard digits 0–9 with your custom font character codes
|
||||
for (int i = 0; i < msg.length(); i++) {
|
||||
if (isDigit(msg[i])) {
|
||||
int num = msg[i] - '0'; // 0–9
|
||||
msg[i] = 145 + ((num + 9) % 10); // Maps 0→154, 1→145, ... 9→153
|
||||
}
|
||||
}
|
||||
|
||||
// --- Determine if we need left padding based on previous mode ---
|
||||
bool addPadding = false;
|
||||
bool humidityVisible = showHumidity && weatherAvailable && strlen(openWeatherApiKey) == 32 && strlen(openWeatherCity) > 0 && strlen(openWeatherCountry) > 0;
|
||||
|
||||
// If coming from CLOCK mode
|
||||
if (prevDisplayMode == 0 && (showDayOfWeek || colonBlinkEnabled)) {
|
||||
addPadding = true;
|
||||
} else if (prevDisplayMode == 1 && humidityVisible) {
|
||||
addPadding = true;
|
||||
}
|
||||
// Apply padding (4 spaces) if needed
|
||||
if (addPadding) {
|
||||
msg = " " + msg;
|
||||
}
|
||||
|
||||
// --- Display scrolling message ---
|
||||
P.setTextAlignment(PA_LEFT);
|
||||
P.setCharSpacing(1);
|
||||
textEffect_t actualScrollDirection = getEffectiveScrollDirection(PA_SCROLL_LEFT, flipDisplay);
|
||||
extern int messageScrollSpeed; // declare globally once at the top of your sketch
|
||||
P.displayScroll(msg.c_str(), PA_LEFT, actualScrollDirection, messageScrollSpeed);
|
||||
while (!P.displayAnimate()) yield();
|
||||
P.setTextAlignment(PA_CENTER);
|
||||
advanceDisplayMode();
|
||||
yield();
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned long currentMillis = millis();
|
||||
unsigned long runtimeSeconds = (currentMillis - bootMillis) / 1000;
|
||||
unsigned long currentTotal = totalUptimeSeconds + runtimeSeconds;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -47,7 +47,7 @@ MD_MAX72XX::fontType_t mFactory[] PROGMEM =
|
||||
17, 130, 186, 198, 254, 134, 234, 134, 254, 250, 130, 250, 254, 134, 234, 134, 254, 124, // 41 - ')'
|
||||
20, 250, 130, 250, 254, 130, 170, 186, 254, 130, 250, 226, 250, 134, 254, 130, 234, 234, 246, 254, 124, // 42 - '*'
|
||||
5, 8, 8, 62, 8, 8, // 43 - '+'
|
||||
1, 192, // 44 - ','
|
||||
2, 128, 64, // 44 - ','
|
||||
2, 8, 8, // 45 - '-'
|
||||
1, 64, // 46 - '.'
|
||||
3, 96, 24, 6, // 47 - '/'
|
||||
|
||||
@@ -28,9 +28,9 @@ MD_Parola P = MD_Parola(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, MAX_DEVICES);
|
||||
AsyncWebServer server(80);
|
||||
|
||||
// --- Global Scroll Speed Settings ---
|
||||
const int GENERAL_SCROLL_SPEED = 85; // Default: Adjust this for Weather Description and Countdown Label (e.g., 50 for faster, 200 for slower)
|
||||
const int IP_SCROLL_SPEED = 115; // Default: Adjust this for the IP Address display (slower for readability)
|
||||
|
||||
const int GENERAL_SCROLL_SPEED = 85; // Default: Adjust this for Weather Description and Countdown Label (e.g., 50 for faster, 200 for slower)
|
||||
const int IP_SCROLL_SPEED = 115; // Default: Adjust this for the IP Address display (slower for readability)
|
||||
int messageScrollSpeed = 85; // default fallback
|
||||
|
||||
// --- Nightscout setting ---
|
||||
const unsigned int NIGHTSCOUT_IDLE_THRESHOLD_MIN = 10; // minutes before data is considered outdated
|
||||
@@ -60,6 +60,8 @@ bool showHumidity = false;
|
||||
bool colonBlinkEnabled = true;
|
||||
char ntpServer1[64] = "pool.ntp.org";
|
||||
char ntpServer2[256] = "time.nist.gov";
|
||||
char customMessage[121] = "";
|
||||
char lastPersistentMessage[128] = "";
|
||||
|
||||
// Dimming
|
||||
bool dimmingEnabled = false;
|
||||
@@ -102,6 +104,8 @@ bool shouldFetchWeatherNow = false;
|
||||
unsigned long lastSwitch = 0;
|
||||
unsigned long lastColonBlink = 0;
|
||||
int displayMode = 0; // 0: Clock, 1: Weather, 2: Weather Description, 3: Countdown
|
||||
int prevDisplayMode = -1;
|
||||
bool clockScrollDone = false;
|
||||
int currentHumidity = -1;
|
||||
bool ntpSyncSuccessful = false;
|
||||
|
||||
@@ -148,7 +152,11 @@ const unsigned long descriptionScrollPause = 300; // 300ms pause after scroll
|
||||
|
||||
// --- Safe WiFi credential and API getters ---
|
||||
const char *getSafeSsid() {
|
||||
return isAPMode ? "********" : ssid;
|
||||
if (isAPMode && strlen(ssid) == 0) {
|
||||
return "";
|
||||
} else {
|
||||
return isAPMode ? "********" : ssid;
|
||||
}
|
||||
}
|
||||
|
||||
const char *getSafePassword() {
|
||||
@@ -253,10 +261,12 @@ void loadConfig() {
|
||||
|
||||
strlcpy(ssid, doc["ssid"] | "", sizeof(ssid));
|
||||
strlcpy(password, doc["password"] | "", sizeof(password));
|
||||
strlcpy(openWeatherApiKey, doc["openWeatherApiKey"] | "", sizeof(openWeatherApiKey)); // Corrected typo here
|
||||
strlcpy(openWeatherApiKey, doc["openWeatherApiKey"] | "", sizeof(openWeatherApiKey));
|
||||
strlcpy(openWeatherCity, doc["openWeatherCity"] | "", sizeof(openWeatherCity));
|
||||
strlcpy(openWeatherCountry, doc["openWeatherCountry"] | "", sizeof(openWeatherCountry));
|
||||
strlcpy(weatherUnits, doc["weatherUnits"] | "metric", sizeof(weatherUnits));
|
||||
strlcpy(customMessage, doc["customMessage"] | "", sizeof(customMessage));
|
||||
strlcpy(lastPersistentMessage, customMessage, sizeof(lastPersistentMessage));
|
||||
clockDuration = doc["clockDuration"] | 10000;
|
||||
weatherDuration = doc["weatherDuration"] | 5000;
|
||||
strlcpy(timeZone, doc["timeZone"] | "Etc/UTC", sizeof(timeZone));
|
||||
@@ -327,7 +337,6 @@ void loadConfig() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// WiFi Setup
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -484,6 +493,7 @@ void clearWiFiCredentialsInConfig() {
|
||||
Serial.println(F("[SECURITY] Cleared WiFi credentials in config.json."));
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Time / NTP Functions
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -521,6 +531,7 @@ void setupTime() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Utility
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -586,6 +597,8 @@ void printConfigToSerial() {
|
||||
Serial.println(countdownLabel);
|
||||
Serial.print(F("Dramatic Countdown Display: "));
|
||||
Serial.println(isDramaticCountdown ? "Yes" : "No");
|
||||
Serial.print(F("Custom Message: "));
|
||||
Serial.println(customMessage);
|
||||
Serial.print(F("Total Runtime: "));
|
||||
if (totalUptimeSeconds > 0) {
|
||||
Serial.println(formatUptime(totalUptimeSeconds));
|
||||
@@ -795,6 +808,7 @@ void setupWebServer() {
|
||||
|
||||
Serial.println(F("[SAVE] Config verification successful."));
|
||||
DynamicJsonDocument okDoc(128);
|
||||
strlcpy(customMessage, doc["customMessage"] | "", sizeof(customMessage));
|
||||
okDoc[F("message")] = "Saved successfully. Rebooting...";
|
||||
String response;
|
||||
serializeJson(okDoc, response);
|
||||
@@ -1105,6 +1119,110 @@ void setupWebServer() {
|
||||
request->send(200, "application/json", "{\"ok\":true}");
|
||||
});
|
||||
|
||||
// --- Custom Message Endpoint ---
|
||||
server.on("/set_custom_message", HTTP_POST, [](AsyncWebServerRequest *request) {
|
||||
if (request->hasParam("message", true)) {
|
||||
String msg = request->getParam("message", true)->value();
|
||||
msg.trim();
|
||||
|
||||
String sourceHeader = request->header("X-Source");
|
||||
bool isFromUI = (sourceHeader == "UI");
|
||||
bool isFromHA = !isFromUI;
|
||||
|
||||
// --- Local speed variable (does not modify global GENERAL_SCROLL_SPEED) ---
|
||||
int localSpeed = GENERAL_SCROLL_SPEED; // Default for UI messages
|
||||
if (request->hasParam("speed", true)) {
|
||||
localSpeed = constrain(request->getParam("speed", true)->value().toInt(), 10, 200);
|
||||
Serial.printf("[MESSAGE] Custom Message scroll speed set to %d\n", localSpeed);
|
||||
}
|
||||
|
||||
// --- CLEAR MESSAGE ---
|
||||
if (msg.length() == 0) {
|
||||
if (isFromUI) {
|
||||
// Web UI clear: remove everything
|
||||
customMessage[0] = '\0';
|
||||
lastPersistentMessage[0] = '\0';
|
||||
displayMode = 0;
|
||||
Serial.println(F("[MESSAGE] All messages cleared by UI. Returning to normal mode."));
|
||||
request->send(200, "text/plain", "CLEARED (UI)");
|
||||
|
||||
// --- SAVE CLEAR STATE ---
|
||||
saveCustomMessageToConfig("");
|
||||
} else {
|
||||
// HA clear: remove only temporary message
|
||||
customMessage[0] = '\0';
|
||||
|
||||
if (strlen(lastPersistentMessage) > 0) {
|
||||
// Restore the last persistent message
|
||||
strncpy(customMessage, lastPersistentMessage, sizeof(customMessage));
|
||||
messageScrollSpeed = GENERAL_SCROLL_SPEED; // Use global speed for persistent
|
||||
Serial.printf("[MESSAGE] Temporary HA message cleared. Restored persistent message: '%s' (speed=%d)\n",
|
||||
customMessage, messageScrollSpeed);
|
||||
request->send(200, "text/plain", "CLEARED (HA temporary, persistent restored)");
|
||||
} else {
|
||||
displayMode = 0;
|
||||
Serial.println(F("[MESSAGE] Temporary HA message cleared. No persistent message to restore."));
|
||||
request->send(200, "text/plain", "CLEARED (HA temporary, no persistent)");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// --- SANITIZE MESSAGE ---
|
||||
msg.toUpperCase();
|
||||
String filtered = "";
|
||||
for (size_t i = 0; i < msg.length(); i++) {
|
||||
char c = msg[i];
|
||||
if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == ' ' || c == ':' || c == '!' || c == '\'' || c == '-' || c == '.' || c == ',' || c == '_' || c == '+' || c == '%' || c == '/' || c == '?') {
|
||||
filtered += c;
|
||||
}
|
||||
}
|
||||
|
||||
filtered.toCharArray(customMessage, sizeof(customMessage));
|
||||
|
||||
// --- STORE MESSAGE ---
|
||||
if (isFromHA) {
|
||||
// --- Preserve the current persistent message before overwriting ---
|
||||
char prevMessage[sizeof(customMessage)];
|
||||
strlcpy(prevMessage, customMessage, sizeof(prevMessage));
|
||||
|
||||
// --- Overwrite customMessage with new temporary HA message ---
|
||||
filtered.toCharArray(customMessage, sizeof(customMessage));
|
||||
messageScrollSpeed = localSpeed; // Use HA-specified scroll speed
|
||||
|
||||
// --- If no persistent message stored yet, keep the previous one ---
|
||||
if (strlen(lastPersistentMessage) == 0 && strlen(prevMessage) > 0) {
|
||||
strlcpy(lastPersistentMessage, prevMessage, sizeof(lastPersistentMessage));
|
||||
}
|
||||
|
||||
Serial.printf("[HA] Temporary HA message received: %s (persistent=%s)\n",
|
||||
customMessage, lastPersistentMessage);
|
||||
|
||||
} else {
|
||||
// --- UI-originated message: permanent ---
|
||||
filtered.toCharArray(customMessage, sizeof(customMessage));
|
||||
strlcpy(lastPersistentMessage, customMessage, sizeof(lastPersistentMessage));
|
||||
messageScrollSpeed = GENERAL_SCROLL_SPEED; // Always global for UI
|
||||
|
||||
Serial.printf("[UI] Persistent message stored: %s (speed=%d)\n",
|
||||
customMessage, messageScrollSpeed);
|
||||
|
||||
// --- Persist to config.json immediately ---
|
||||
saveCustomMessageToConfig(customMessage);
|
||||
}
|
||||
|
||||
// --- Activate display ---
|
||||
displayMode = 6;
|
||||
prevDisplayMode = 0;
|
||||
|
||||
String response = String(isFromHA ? "OK (HA message, speed=" : "OK (UI message, speed=") + String(localSpeed) + ")";
|
||||
request->send(200, "text/plain", response);
|
||||
} else {
|
||||
Serial.println(F("[MESSAGE] Error: missing 'message' parameter in request."));
|
||||
request->send(400, "text/plain", "Missing message parameter");
|
||||
}
|
||||
});
|
||||
|
||||
server.on("/uptime", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
if (!LittleFS.exists("/uptime.dat")) {
|
||||
request->send(200, "text/plain", "No uptime recorded yet.");
|
||||
@@ -1604,6 +1722,7 @@ void fetchWeather() {
|
||||
http.end();
|
||||
}
|
||||
|
||||
|
||||
void loadUptime() {
|
||||
if (LittleFS.exists("/uptime.dat")) {
|
||||
File f = LittleFS.open("/uptime.dat", "r");
|
||||
@@ -1640,6 +1759,41 @@ void saveUptime() {
|
||||
}
|
||||
}
|
||||
|
||||
void saveCustomMessageToConfig(const char *msg) {
|
||||
Serial.println(F("[CONFIG] Updating customMessage in config.json..."));
|
||||
|
||||
DynamicJsonDocument doc(2048);
|
||||
|
||||
// Load existing config.json (if present)
|
||||
File configFile = LittleFS.open("/config.json", "r");
|
||||
if (configFile) {
|
||||
DeserializationError err = deserializeJson(doc, configFile);
|
||||
configFile.close();
|
||||
if (err) {
|
||||
Serial.print(F("[CONFIG] Error reading existing config: "));
|
||||
Serial.println(err.f_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Update only customMessage
|
||||
doc["customMessage"] = msg;
|
||||
|
||||
// Safely write back to config.json
|
||||
if (LittleFS.exists("/config.json")) {
|
||||
LittleFS.rename("/config.json", "/config.bak");
|
||||
}
|
||||
|
||||
File f = LittleFS.open("/config.json", "w");
|
||||
if (!f) {
|
||||
Serial.println(F("[CONFIG] ERROR: Failed to open /config.json for writing"));
|
||||
return;
|
||||
}
|
||||
|
||||
size_t bytesWritten = serializeJson(doc, f);
|
||||
f.close();
|
||||
Serial.printf("[CONFIG] Saved customMessage='%s' (%u bytes written)\n", msg, bytesWritten);
|
||||
}
|
||||
|
||||
// Returns formatted uptime (for web UI or logs)
|
||||
String formatUptime(unsigned long seconds) {
|
||||
unsigned long days = seconds / 86400;
|
||||
@@ -1656,7 +1810,6 @@ String formatUptime(unsigned long seconds) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Main setup() and loop()
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -1667,8 +1820,9 @@ DisplayMode key:
|
||||
2: Weather Description
|
||||
3: Countdown
|
||||
4: Nightscout
|
||||
5: Date
|
||||
6: Custom Message
|
||||
*/
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
@@ -1719,7 +1873,9 @@ void setup() {
|
||||
saveUptime();
|
||||
}
|
||||
|
||||
|
||||
void advanceDisplayMode() {
|
||||
prevDisplayMode = displayMode;
|
||||
int oldMode = displayMode;
|
||||
String ntpField = String(ntpServer2);
|
||||
bool nightscoutConfigured = ntpField.startsWith("https://");
|
||||
@@ -1788,18 +1944,26 @@ void advanceDisplayMode() {
|
||||
displayMode = 0;
|
||||
Serial.println(F("[DISPLAY] Switching to display mode: CLOCK (from Countdown)"));
|
||||
}
|
||||
} else if (displayMode == 4) { // Nightscout -> Clock
|
||||
} else if (displayMode == 4) { // Nightscout -> Custom Message
|
||||
displayMode = 6;
|
||||
Serial.println(F("[DISPLAY] Switching to display mode: CUSTOM MESSAGE (from Nightscout)"));
|
||||
} else if (displayMode == 6) { // Custom Message -> Clock
|
||||
displayMode = 0;
|
||||
Serial.println(F("[DISPLAY] Switching to display mode: CLOCK (from Nightscout)"));
|
||||
Serial.println(F("[DISPLAY] Switching to display mode: CLOCK (from Custom Message)"));
|
||||
}
|
||||
|
||||
// --- Common cleanup/reset logic remains the same ---
|
||||
if ((displayMode == 0) && strlen(customMessage) > 0 && oldMode != 6) {
|
||||
displayMode = 6;
|
||||
Serial.println(F("[DISPLAY] Custom Message display before returning to CLOCK"));
|
||||
}
|
||||
lastSwitch = millis();
|
||||
}
|
||||
|
||||
|
||||
void advanceDisplayModeSafe() {
|
||||
int attempts = 0;
|
||||
const int MAX_ATTEMPTS = 6; // Number of possible modes + 1
|
||||
const int MAX_ATTEMPTS = 7; // Number of possible modes + 1
|
||||
int startMode = displayMode;
|
||||
bool valid = false;
|
||||
do {
|
||||
@@ -1816,6 +1980,7 @@ void advanceDisplayModeSafe() {
|
||||
else if (displayMode == 2 && showWeatherDescription && weatherAvailable && weatherDescription.length() > 0) valid = true;
|
||||
else if (displayMode == 3 && countdownEnabled && !countdownFinished && ntpSyncSuccessful) valid = true;
|
||||
else if (displayMode == 4 && nightscoutConfigured) valid = true;
|
||||
else if (displayMode == 6 && strlen(customMessage) > 0) valid = true;
|
||||
|
||||
// If we've looped back to where we started, break to avoid infinite loop
|
||||
if (displayMode == startMode) break;
|
||||
@@ -1831,6 +1996,7 @@ void advanceDisplayModeSafe() {
|
||||
lastSwitch = millis();
|
||||
}
|
||||
|
||||
|
||||
//config save after countdown finishes
|
||||
bool saveCountdownConfig(bool enabled, time_t targetTimestamp, const String &label) {
|
||||
DynamicJsonDocument doc(2048);
|
||||
@@ -1894,7 +2060,6 @@ void loop() {
|
||||
const unsigned long fetchInterval = 300000; // 5 minutes
|
||||
|
||||
|
||||
|
||||
// AP Mode animation
|
||||
static unsigned long apAnimTimer = 0;
|
||||
static int apAnimFrame = 0;
|
||||
@@ -2086,7 +2251,6 @@ void loop() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Only advance mode by timer for clock/weather, not description!
|
||||
unsigned long displayDuration = (displayMode == 0) ? clockDuration : weatherDuration;
|
||||
if ((displayMode == 0 || displayMode == 1) && millis() - lastSwitch > displayDuration) {
|
||||
@@ -2094,7 +2258,6 @@ void loop() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --- MODIFIED WEATHER FETCHING LOGIC ---
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
if (!weatherFetchInitiated || shouldFetchWeatherNow || (millis() - lastFetch > fetchInterval)) {
|
||||
@@ -2117,7 +2280,6 @@ void loop() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
const char *const *daysOfTheWeek = getDaysOfWeek(language);
|
||||
const char *daySymbol = daysOfTheWeek[timeinfo.tm_wday];
|
||||
|
||||
@@ -2174,9 +2336,6 @@ void loop() {
|
||||
advanceDisplayMode();
|
||||
}
|
||||
|
||||
// Persistent variables (declare near top of file or loop)
|
||||
static int prevDisplayMode = -1;
|
||||
static bool clockScrollDone = false;
|
||||
|
||||
// --- CLOCK Display Mode ---
|
||||
if (displayMode == 0) {
|
||||
@@ -2226,6 +2385,8 @@ void loop() {
|
||||
shouldScrollIn = true; // first boot or other special modes
|
||||
} else if (prevDisplayMode == 2 && weatherDescription.length() > 8) {
|
||||
shouldScrollIn = true; // only scroll in if weather was scrolling
|
||||
} else if (prevDisplayMode == 6) {
|
||||
shouldScrollIn = true; // scroll in when coming from custom message
|
||||
}
|
||||
|
||||
if (shouldScrollIn && !clockScrollDone) {
|
||||
@@ -2254,9 +2415,6 @@ void loop() {
|
||||
}
|
||||
}
|
||||
|
||||
// --- update prevDisplayMode ---
|
||||
prevDisplayMode = displayMode;
|
||||
|
||||
|
||||
// --- WEATHER Display Mode ---
|
||||
static bool weatherWasAvailable = false;
|
||||
@@ -2297,6 +2455,18 @@ void loop() {
|
||||
if (displayMode == 2 && showWeatherDescription && weatherAvailable && weatherDescription.length() > 0) {
|
||||
String desc = weatherDescription;
|
||||
|
||||
// --- Check if humidity is actually visible ---
|
||||
bool humidityVisible = showHumidity && weatherAvailable && strlen(openWeatherApiKey) == 32 && strlen(openWeatherCity) > 0 && strlen(openWeatherCountry) > 0;
|
||||
|
||||
// --- Conditional padding ---
|
||||
bool addPadding = false;
|
||||
if (prevDisplayMode == 1 && humidityVisible) {
|
||||
addPadding = true;
|
||||
}
|
||||
if (addPadding) {
|
||||
desc = " " + desc; // 4-space padding before scrolling
|
||||
}
|
||||
|
||||
// prepare safe buffer
|
||||
static char descBuffer[128]; // large enough for OWM translations
|
||||
desc.toCharArray(descBuffer, sizeof(descBuffer));
|
||||
@@ -2609,10 +2779,17 @@ void loop() {
|
||||
}
|
||||
|
||||
String fullString = String(buf);
|
||||
bool addPadding = false;
|
||||
bool humidityVisible = showHumidity && weatherAvailable && strlen(openWeatherApiKey) == 32 && strlen(openWeatherCity) > 0 && strlen(openWeatherCountry) > 0;
|
||||
|
||||
// --- Add a leading space only if showDayOfWeek is true ---
|
||||
if (showDayOfWeek) {
|
||||
fullString = " " + fullString;
|
||||
// Padding logic
|
||||
if (prevDisplayMode == 0 && (showDayOfWeek || colonBlinkEnabled)) {
|
||||
addPadding = true;
|
||||
} else if (prevDisplayMode == 1 && humidityVisible) {
|
||||
addPadding = true;
|
||||
}
|
||||
if (addPadding) {
|
||||
fullString = " " + fullString; // 4 spaces
|
||||
}
|
||||
|
||||
// Display the full string and scroll it
|
||||
@@ -2890,6 +3067,53 @@ void loop() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Custom Message Display Mode (displayMode == 6) ---
|
||||
if (displayMode == 6) {
|
||||
if (strlen(customMessage) == 0) {
|
||||
advanceDisplayMode();
|
||||
yield();
|
||||
return;
|
||||
}
|
||||
|
||||
String msg = String(customMessage);
|
||||
|
||||
// Replace standard digits 0–9 with your custom font character codes
|
||||
for (int i = 0; i < msg.length(); i++) {
|
||||
if (isDigit(msg[i])) {
|
||||
int num = msg[i] - '0'; // 0–9
|
||||
msg[i] = 145 + ((num + 9) % 10); // Maps 0→154, 1→145, ... 9→153
|
||||
}
|
||||
}
|
||||
|
||||
// --- Determine if we need left padding based on previous mode ---
|
||||
bool addPadding = false;
|
||||
bool humidityVisible = showHumidity && weatherAvailable && strlen(openWeatherApiKey) == 32 && strlen(openWeatherCity) > 0 && strlen(openWeatherCountry) > 0;
|
||||
|
||||
// If coming from CLOCK mode
|
||||
if (prevDisplayMode == 0 && (showDayOfWeek || colonBlinkEnabled)) {
|
||||
addPadding = true;
|
||||
} else if (prevDisplayMode == 1 && humidityVisible) {
|
||||
addPadding = true;
|
||||
}
|
||||
// Apply padding (4 spaces) if needed
|
||||
if (addPadding) {
|
||||
msg = " " + msg;
|
||||
}
|
||||
|
||||
// --- Display scrolling message ---
|
||||
P.setTextAlignment(PA_LEFT);
|
||||
P.setCharSpacing(1);
|
||||
textEffect_t actualScrollDirection = getEffectiveScrollDirection(PA_SCROLL_LEFT, flipDisplay);
|
||||
extern int messageScrollSpeed; // declare globally once at the top of your sketch
|
||||
P.displayScroll(msg.c_str(), PA_LEFT, actualScrollDirection, messageScrollSpeed);
|
||||
while (!P.displayAnimate()) yield();
|
||||
P.setTextAlignment(PA_CENTER);
|
||||
advanceDisplayMode();
|
||||
yield();
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned long currentMillis = millis();
|
||||
unsigned long runtimeSeconds = (currentMillis - bootMillis) / 1000;
|
||||
unsigned long currentTotal = totalUptimeSeconds + runtimeSeconds;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -47,7 +47,7 @@ MD_MAX72XX::fontType_t mFactory[] PROGMEM =
|
||||
17, 130, 186, 198, 254, 134, 234, 134, 254, 250, 130, 250, 254, 134, 234, 134, 254, 124, // 41 - ')'
|
||||
20, 250, 130, 250, 254, 130, 170, 186, 254, 130, 250, 226, 250, 134, 254, 130, 234, 234, 246, 254, 124, // 42 - '*'
|
||||
5, 8, 8, 62, 8, 8, // 43 - '+'
|
||||
1, 192, // 44 - ','
|
||||
2, 128, 64, // 44 - ','
|
||||
2, 8, 8, // 45 - '-'
|
||||
1, 64, // 46 - '.'
|
||||
3, 96, 24, 6, // 47 - '/'
|
||||
|
||||
Reference in New Issue
Block a user