Clock-Only Dim Mode + Safe Config Migration & USB Stability

- Implemented safe config migration for new JSON key
- Auto-update HTML UI when version changes
- Improve USB CDC serial stability on native USB boards
- Avoid blocking serial dumps on native USB boards
- Added “Clock-Only Mode When Dimmed” feature
- Cross-board stability validation (ESP8266 / ESP32 / S2)
This commit is contained in:
M-Factory
2026-01-23 14:59:44 +09:00
parent 047c64adc0
commit 8121e5d1a9
5 changed files with 523 additions and 4 deletions

View File

@@ -91,6 +91,7 @@ int sunriseHour = 6;
int sunriseMinute = 0;
int sunsetHour = 18;
int sunsetMinute = 0;
bool clockOnlyDuringDimming = false;
//Countdown Globals
bool countdownEnabled = false;
@@ -251,6 +252,7 @@ void loadConfig() {
doc[F("sunriseMinute")] = sunriseMinute;
doc[F("sunsetHour")] = sunsetHour;
doc[F("sunsetMinute")] = sunsetMinute;
doc[F("clockOnlyDuringDimming")] = false;
// Add countdown defaults when creating a new config.json
JsonObject countdownObj = doc.createNestedObject("countdown");
@@ -286,6 +288,8 @@ void loadConfig() {
return;
}
bool configChanged = false;
strlcpy(ssid, doc["ssid"] | "", sizeof(ssid));
strlcpy(password, doc["password"] | "", sizeof(password));
strlcpy(openWeatherApiKey, doc["openWeatherApiKey"] | "", sizeof(openWeatherApiKey));
@@ -393,6 +397,31 @@ void loadConfig() {
Serial.println(F("[CONFIG] Countdown object not found, defaulting to disabled."));
countdownFinished = false;
}
// --- CLOCK-ONLY-DURING-DIMMING LOADING ---
if (doc.containsKey("clockOnlyDuringDimming")) {
clockOnlyDuringDimming = doc["clockOnlyDuringDimming"].as<bool>();
} else {
clockOnlyDuringDimming = false;
doc["clockOnlyDuringDimming"] = clockOnlyDuringDimming;
configChanged = true;
Serial.println(F("[CONFIG] Migrated: added clockOnlyDuringDimming default."));
}
// --- Save migrated config if needed ---
if (configChanged) {
Serial.println(F("[CONFIG] Saving migrated config.json"));
File f = LittleFS.open("/config.json", "w");
if (f) {
serializeJsonPretty(doc, f);
f.close();
Serial.println(F("[CONFIG] Migration saved successfully."));
} else {
Serial.println(F("[ERROR] Failed to save migrated config.json"));
}
}
Serial.println(F("[CONFIG] Configuration loaded."));
}
@@ -612,6 +641,8 @@ void printConfigToSerial() {
Serial.println(autoDimmingEnabled ? "Enabled" : "Disabled");
Serial.print(F("Custom Dimming: "));
Serial.println(dimmingEnabled ? "Enabled" : "Disabled");
Serial.print(F("Clock only during dimming: "));
Serial.println(clockOnlyDuringDimming ? "Yes" : "No");
if (autoDimmingEnabled) {
// --- Automatic (Sunrise/Sunset) dimming mode ---
@@ -676,7 +707,12 @@ void setupWebServer() {
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
Serial.println(F("[WEBSERVER] Request: /"));
request->send(LittleFS, "/index.html", "text/html");
// Create a response from LittleFS file so we can attach cache-control headers
AsyncWebServerResponse *response = request->beginResponse(LittleFS, "/index.html", "text/html");
response->addHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response->addHeader("Pragma", "no-cache");
response->addHeader("Expires", "0");
request->send(response);
});
server.on("/generate_204", HTTP_GET, handleCaptivePortal); // Android
@@ -781,7 +817,9 @@ void setupWebServer() {
else doc[n] = v.toInt();
} else if (n == "showWeatherDescription") doc[n] = (v == "true" || v == "on" || v == "1");
else if (n == "dimmingEnabled") doc[n] = (v == "true" || v == "on" || v == "1");
else if (n == "weatherUnits") doc[n] = v;
else if (n == "clockOnlyDuringDimming") {
doc[n] = (v == "true" || v == "on" || v == "1");
} else if (n == "weatherUnits") doc[n] = v;
else if (n == "password") {
if (v != "********" && v.length() > 0) {
@@ -1203,6 +1241,85 @@ void setupWebServer() {
request->send(200, "application/json", "{\"ok\":true}");
});
// Set Clock-only-during-dimming (no reboot)
server.on("/set_clock_only_dimming", HTTP_POST, [](AsyncWebServerRequest *request) {
bool enableNow = false;
if (request->hasParam("value", true)) {
String v = request->getParam("value", true)->value();
enableNow = (v == "1" || v == "true" || v == "on");
}
// Update runtime variable immediately
clockOnlyDuringDimming = enableNow;
Serial.printf("[WEBSERVER] Set clockOnlyDuringDimming to %d (requested)\n", clockOnlyDuringDimming);
// Read existing config.json (if present)
DynamicJsonDocument doc(2048);
bool needToWrite = true;
File configFile = LittleFS.open("/config.json", "r");
if (configFile) {
DeserializationError err = deserializeJson(doc, configFile);
configFile.close();
if (err) {
Serial.print(F("[WEBSERVER] Error parsing existing config.json: "));
Serial.println(err.f_str());
// proceed to write (will create a new doc)
doc.clear();
} else {
// If the key exists and matches the requested value, skip write
bool existing = doc["clockOnlyDuringDimming"] | false;
if (existing == enableNow) {
Serial.println(F("[WEBSERVER] clockOnlyDuringDimming unchanged — skipping write."));
// Send immediate OK response without touching FS
DynamicJsonDocument okDoc(128);
okDoc[F("ok")] = true;
okDoc[F("clockOnlyDuringDimming")] = enableNow;
String response;
serializeJson(okDoc, response);
request->send(200, "application/json", response);
return;
}
}
} else {
// No config file found — doc is empty and we will write
doc.clear();
}
// Set/update the key in the JSON doc
doc[F("clockOnlyDuringDimming")] = clockOnlyDuringDimming;
// Backup existing file only if it exists (and only because we're about to replace it)
if (LittleFS.exists("/config.json")) {
if (!LittleFS.rename("/config.json", "/config.bak")) {
Serial.println(F("[WEBSERVER] Warning: failed to create config backup"));
// continue anyway
}
}
File f = LittleFS.open("/config.json", "w");
if (!f) {
Serial.println(F("[WEBSERVER] ERROR: Failed to open /config.json for writing"));
DynamicJsonDocument errDoc(128);
errDoc[F("error")] = "Failed to write config file.";
String response;
serializeJson(errDoc, response);
request->send(500, "application/json", response);
return;
}
size_t bytesWritten = serializeJson(doc, f);
f.close();
Serial.printf("[WEBSERVER] Saved clockOnlyDuringDimming=%d to /config.json (%u bytes written)\n", clockOnlyDuringDimming, bytesWritten);
// Send immediate response (no reboot)
DynamicJsonDocument okDoc(128);
okDoc[F("ok")] = true;
okDoc[F("clockOnlyDuringDimming")] = clockOnlyDuringDimming;
String response;
serializeJson(okDoc, response);
request->send(200, "application/json", response);
});
// --- Custom Message Endpoint ---
server.on("/set_custom_message", HTTP_POST, [](AsyncWebServerRequest *request) {
if (request->hasParam("message", true)) {
@@ -2158,6 +2275,11 @@ DisplayMode key:
void setup() {
Serial.begin(115200);
delay(1000);
#if defined(ARDUINO_USB_MODE)
Serial.setTxTimeoutMs(50);
Serial.println("[SERIAL] USB CDC detected — TX timeout enabled");
delay(500);
#endif
Serial.println();
Serial.println(F("[SETUP] Starting setup..."));
@@ -2244,7 +2366,9 @@ void setup() {
Serial.println(F("[SETUP] Webserver setup complete"));
Serial.println(F("[SETUP] Setup complete"));
Serial.println();
#if !defined(ARDUINO_USB_MODE)
printConfigToSerial();
#endif
setupTime();
displayMode = 0;
lastSwitch = millis() - (clockDuration - 500);
@@ -2318,6 +2442,39 @@ void ensureHtmlFileExists() {
}
void advanceDisplayMode() {
// If user requested clock-only during dimming and we are currently dimmed, stay on clock
if (clockOnlyDuringDimming) {
time_t now = time(nullptr);
struct tm local_tm;
localtime_r(&now, &local_tm);
int curTotal = local_tm.tm_hour * 60 + local_tm.tm_min;
int startTotal = -1, endTotal = -1;
bool currentlyDimmed = false;
if (autoDimmingEnabled) {
startTotal = sunsetHour * 60 + sunsetMinute;
endTotal = sunriseHour * 60 + sunriseMinute;
currentlyDimmed = (startTotal < endTotal)
? (curTotal >= startTotal && curTotal < endTotal)
: (curTotal >= startTotal || curTotal < endTotal);
} else if (dimmingEnabled) {
startTotal = dimStartHour * 60 + dimStartMinute;
endTotal = dimEndHour * 60 + dimEndMinute;
currentlyDimmed = (startTotal < endTotal)
? (curTotal >= startTotal && curTotal < endTotal)
: (curTotal >= startTotal || curTotal < endTotal);
}
if (currentlyDimmed) {
displayMode = 0;
lastSwitch = millis();
Serial.println(F("[DISPLAY] advanceDisplayMode(): Staying in CLOCK because Clock-only-dimming is enabled and dimming is active."));
return;
}
}
prevDisplayMode = displayMode;
int oldMode = displayMode;
String ntpField = String(ntpServer2);
@@ -2599,6 +2756,15 @@ void loop() {
P.setIntensity(targetBrightness);
}
// Enforce "Clock only during dimming" if enabled
if (clockOnlyDuringDimming && dimActive) {
if (displayMode != 0) {
prevDisplayMode = displayMode;
displayMode = 0;
lastSwitch = millis();
Serial.println(F("[DISPLAY] Forcing CLOCK because 'Clock only during dimming' is enabled and dimming is active."));
}
}
// --- IMMEDIATE COUNTDOWN FINISH TRIGGER ---
if (countdownEnabled && !countdownFinished && ntpSyncSuccessful && countdownTargetTimestamp > 0 && now_time >= countdownTargetTimestamp) {

View File

@@ -7,6 +7,9 @@ const char index_html[] PROGMEM = R"rawliteral(
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<title>ESPTimeCast Settings</title>
<style>
@@ -611,6 +614,15 @@ textarea::placeholder {
</label>
<label label style="display: flex; align-items: center; justify-content: space-between; margin-top: 0.75rem;">
<span style="margin-right: 0.5em;">Clock-Only Mode When Dimmed:</span>
<span class="toggle-switch">
<input type="checkbox" id="clockOnlyDuringDimming">
<span class="toggle-slider"></span>
</span>
</label>
<div class="form-row two-col">
<div>
<label for="dimStartTime">Start Time:</label>
@@ -798,15 +810,20 @@ window.onload = function () {
// Apply initial dimming state
setDimmingFieldsEnabled();
// Initialize Clock-only-during-dimming control (depends on dimming fields)
initClockOnlyDuringDimming(data);
// Attach listeners (mutually exclusive + API dependency)
if (apiInputEl) apiInputEl.addEventListener('input', setDimmingFieldsEnabled);
autoDimmingEl.addEventListener('change', () => {
if (autoDimmingEl.checked) dimmingEnabledEl.checked = false;
setDimmingFieldsEnabled();
clearClockOnlyIfNoDimming();
});
dimmingEnabledEl.addEventListener('change', () => {
if (dimmingEnabledEl.checked) autoDimmingEl.checked = false;
setDimmingFieldsEnabled();
clearClockOnlyIfNoDimming();
});
// Set field values from config
@@ -1325,6 +1342,48 @@ function setWeatherUnits(val) {
});
}
// --- Clock-only-during-dimming setter (no reboot) ---
function setClockOnlyDuringDimming(val) {
fetch('/set_clock_only_dimming', {
method: 'POST',
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "value=" + (val ? 1 : 0)
}).catch(e => {
console.error('Failed to set clockOnlyDuringDimming:', e);
});
}
// Initialize the checkbox from cfg and wire up immediate save (no reboot)
function initClockOnlyDuringDimming(cfg) {
const el = document.getElementById('clockOnlyDuringDimming');
if (!el) return;
// Set initial checked value from config
el.checked = !!cfg.clockOnlyDuringDimming;
// Determine whether dimming is enabled (auto OR custom)
const autoDim = (cfg.autoDimmingEnabled === true || cfg.autoDimmingEnabled === "true" || cfg.autoDimmingEnabled === 1);
const manualDim = (cfg.dimmingEnabled === true || cfg.dimmingEnabled === "true" || cfg.dimmingEnabled === 1);
// Normalize dimBrightness from config (handle "Off" or "-1" string)
let db = cfg.dimBrightness;
if (typeof db === 'string') {
if (db.toLowerCase() === 'off') db = -1;
else db = parseInt(db, 10);
}
const dimBrightnessOk = (typeof db === 'number') ? (db !== -1) : true;
// Enable only when some dimming mode is active and dimming does not fully turn display off
el.disabled = !(autoDim || manualDim) || !dimBrightnessOk;
// On change, persist immediately (no reboot)
el.addEventListener('change', function () {
const want = this.checked;
setClockOnlyDuringDimming(want);
// optimistic UI: leave checkbox as toggled; if server fails we don't roll back here
});
}
// --- Countdown Controls Logic ---
// NEW: Function to enable/disable countdown specific fields
function setCountdownFieldsEnabled(enabled) {
@@ -1360,6 +1419,8 @@ function setDimmingFieldsEnabled(enabled) {
document.getElementById('dimBrightness').disabled = !enabled;
}
async function getLocation() {
const normalize = v => {
if (v === null || v === undefined) return '';
@@ -1610,6 +1671,17 @@ function setDimmingFieldsEnabled() {
const isCustomDimmingActive = dimmingEnabled.checked;
const isDimmingActive = isAutoDimmingActive || isCustomDimmingActive; // Brightness slider logic
// --- Update Clock-only-during-dimming checkbox state (if present) ---
const clockOnlyEl = document.getElementById('clockOnlyDuringDimming');
if (clockOnlyEl) {
// Read current brightness control value (string)
const dbEl = document.getElementById('dimBrightness');
const dbVal = dbEl ? dbEl.value : null;
const dbOk = (dbVal !== null) ? !(String(dbVal).toLowerCase() === 'off' || String(dbVal) === '-1') : true;
const currentlyDimEnabled = isAutoDimmingActive || isCustomDimmingActive;
clockOnlyEl.disabled = !currentlyDimEnabled || !dbOk;
}
// BRIGHTNESS SLIDER: Enabled if EITHER mode is active.
if (dimBrightness) {
dimBrightness.disabled = !isDimmingActive;
@@ -1623,6 +1695,31 @@ function setDimmingFieldsEnabled() {
if (dimEnd) {
dimEnd.disabled = !isCustomTimeEnabled;
}
clearClockOnlyIfNoDimming();
}
// If both dimming modes are disabled, clear & persist the Clock-only-during-dimming flag
function clearClockOnlyIfNoDimming() {
const autoEl = document.getElementById('autoDimmingEnabled');
const dimEl = document.getElementById('dimmingEnabled');
const clockEl = document.getElementById('clockOnlyDuringDimming');
if (!autoEl || !dimEl || !clockEl) return;
if (!autoEl.checked && !dimEl.checked) {
// if currently checked, uncheck and persist change immediately
if (clockEl.checked) {
clockEl.checked = false;
// persist without reboot
fetch('/set_clock_only_dimming', {
method: 'POST',
body: new URLSearchParams({ value: 'false' })
}).catch(e => console.error('Failed to persist clockOnlyDuringDimming clear:', e));
}
// also ensure it's disabled in the UI
clockEl.disabled = true;
}
}
window.addEventListener('DOMContentLoaded', () => {

View File

@@ -91,6 +91,7 @@ int sunriseHour = 6;
int sunriseMinute = 0;
int sunsetHour = 18;
int sunsetMinute = 0;
bool clockOnlyDuringDimming = false;
//Countdown Globals - NEW
bool countdownEnabled = false;
@@ -250,6 +251,7 @@ void loadConfig() {
doc[F("sunriseMinute")] = sunriseMinute;
doc[F("sunsetHour")] = sunsetHour;
doc[F("sunsetMinute")] = sunsetMinute;
doc[F("clockOnlyDuringDimming")] = false;
// Add countdown defaults when creating a new config.json
JsonObject countdownObj = doc.createNestedObject("countdown");
@@ -285,6 +287,8 @@ void loadConfig() {
return;
}
bool configChanged = false;
strlcpy(ssid, doc["ssid"] | "", sizeof(ssid));
strlcpy(password, doc["password"] | "", sizeof(password));
strlcpy(openWeatherApiKey, doc["openWeatherApiKey"] | "", sizeof(openWeatherApiKey));
@@ -389,6 +393,31 @@ void loadConfig() {
Serial.println(F("[CONFIG] Countdown object not found, defaulting to disabled."));
countdownFinished = false;
}
// --- CLOCK-ONLY-DURING-DIMMING LOADING ---
if (doc.containsKey("clockOnlyDuringDimming")) {
clockOnlyDuringDimming = doc["clockOnlyDuringDimming"].as<bool>();
} else {
clockOnlyDuringDimming = false;
doc["clockOnlyDuringDimming"] = clockOnlyDuringDimming;
configChanged = true;
Serial.println(F("[CONFIG] Migrated: added clockOnlyDuringDimming default."));
}
// --- Save migrated config if needed ---
if (configChanged) {
Serial.println(F("[CONFIG] Saving migrated config.json"));
File f = LittleFS.open("/config.json", "w");
if (f) {
serializeJsonPretty(doc, f);
f.close();
Serial.println(F("[CONFIG] Migration saved successfully."));
} else {
Serial.println(F("[ERROR] Failed to save migrated config.json"));
}
}
Serial.println(F("[CONFIG] Configuration loaded."));
}
@@ -609,6 +638,8 @@ void printConfigToSerial() {
Serial.println(autoDimmingEnabled ? "Enabled" : "Disabled");
Serial.print(F("Custom Dimming: "));
Serial.println(dimmingEnabled ? "Enabled" : "Disabled");
Serial.print(F("Clock only during dimming: "));
Serial.println(clockOnlyDuringDimming ? "Yes" : "No");
if (autoDimmingEnabled) {
// --- Automatic (Sunrise/Sunset) dimming mode ---
@@ -673,7 +704,12 @@ void setupWebServer() {
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
Serial.println(F("[WEBSERVER] Request: /"));
request->send(LittleFS, "/index.html", "text/html");
// Create a response from LittleFS file so we can attach cache-control headers
AsyncWebServerResponse *response = request->beginResponse(LittleFS, "/index.html", "text/html");
response->addHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response->addHeader("Pragma", "no-cache");
response->addHeader("Expires", "0");
request->send(response);
});
server.on("/generate_204", HTTP_GET, handleCaptivePortal); // Android
@@ -778,7 +814,9 @@ void setupWebServer() {
else doc[n] = v.toInt();
} else if (n == "showWeatherDescription") doc[n] = (v == "true" || v == "on" || v == "1");
else if (n == "dimmingEnabled") doc[n] = (v == "true" || v == "on" || v == "1");
else if (n == "weatherUnits") doc[n] = v;
else if (n == "clockOnlyDuringDimming") {
doc[n] = (v == "true" || v == "on" || v == "1");
} else if (n == "weatherUnits") doc[n] = v;
else if (n == "password") {
if (v != "********" && v.length() > 0) {
@@ -1200,6 +1238,85 @@ void setupWebServer() {
request->send(200, "application/json", "{\"ok\":true}");
});
// Set Clock-only-during-dimming (no reboot)
server.on("/set_clock_only_dimming", HTTP_POST, [](AsyncWebServerRequest *request) {
bool enableNow = false;
if (request->hasParam("value", true)) {
String v = request->getParam("value", true)->value();
enableNow = (v == "1" || v == "true" || v == "on");
}
// Update runtime variable immediately
clockOnlyDuringDimming = enableNow;
Serial.printf("[WEBSERVER] Set clockOnlyDuringDimming to %d (requested)\n", clockOnlyDuringDimming);
// Read existing config.json (if present)
DynamicJsonDocument doc(2048);
bool needToWrite = true;
File configFile = LittleFS.open("/config.json", "r");
if (configFile) {
DeserializationError err = deserializeJson(doc, configFile);
configFile.close();
if (err) {
Serial.print(F("[WEBSERVER] Error parsing existing config.json: "));
Serial.println(err.f_str());
// proceed to write (will create a new doc)
doc.clear();
} else {
// If the key exists and matches the requested value, skip write
bool existing = doc["clockOnlyDuringDimming"] | false;
if (existing == enableNow) {
Serial.println(F("[WEBSERVER] clockOnlyDuringDimming unchanged — skipping write."));
// Send immediate OK response without touching FS
DynamicJsonDocument okDoc(128);
okDoc[F("ok")] = true;
okDoc[F("clockOnlyDuringDimming")] = enableNow;
String response;
serializeJson(okDoc, response);
request->send(200, "application/json", response);
return;
}
}
} else {
// No config file found — doc is empty and we will write
doc.clear();
}
// Set/update the key in the JSON doc
doc[F("clockOnlyDuringDimming")] = clockOnlyDuringDimming;
// Backup existing file only if it exists (and only because we're about to replace it)
if (LittleFS.exists("/config.json")) {
if (!LittleFS.rename("/config.json", "/config.bak")) {
Serial.println(F("[WEBSERVER] Warning: failed to create config backup"));
// continue anyway
}
}
File f = LittleFS.open("/config.json", "w");
if (!f) {
Serial.println(F("[WEBSERVER] ERROR: Failed to open /config.json for writing"));
DynamicJsonDocument errDoc(128);
errDoc[F("error")] = "Failed to write config file.";
String response;
serializeJson(errDoc, response);
request->send(500, "application/json", response);
return;
}
size_t bytesWritten = serializeJson(doc, f);
f.close();
Serial.printf("[WEBSERVER] Saved clockOnlyDuringDimming=%d to /config.json (%u bytes written)\n", clockOnlyDuringDimming, bytesWritten);
// Send immediate response (no reboot)
DynamicJsonDocument okDoc(128);
okDoc[F("ok")] = true;
okDoc[F("clockOnlyDuringDimming")] = clockOnlyDuringDimming;
String response;
serializeJson(okDoc, response);
request->send(200, "application/json", response);
});
// --- Custom Message Endpoint ---
server.on("/set_custom_message", HTTP_POST, [](AsyncWebServerRequest *request) {
if (request->hasParam("message", true)) {
@@ -2310,6 +2427,39 @@ void ensureHtmlFileExists() {
}
void advanceDisplayMode() {
// If user requested clock-only during dimming and we are currently dimmed, stay on clock
if (clockOnlyDuringDimming) {
time_t now = time(nullptr);
struct tm local_tm;
localtime_r(&now, &local_tm);
int curTotal = local_tm.tm_hour * 60 + local_tm.tm_min;
int startTotal = -1, endTotal = -1;
bool currentlyDimmed = false;
if (autoDimmingEnabled) {
startTotal = sunsetHour * 60 + sunsetMinute;
endTotal = sunriseHour * 60 + sunriseMinute;
currentlyDimmed = (startTotal < endTotal)
? (curTotal >= startTotal && curTotal < endTotal)
: (curTotal >= startTotal || curTotal < endTotal);
} else if (dimmingEnabled) {
startTotal = dimStartHour * 60 + dimStartMinute;
endTotal = dimEndHour * 60 + dimEndMinute;
currentlyDimmed = (startTotal < endTotal)
? (curTotal >= startTotal && curTotal < endTotal)
: (curTotal >= startTotal || curTotal < endTotal);
}
if (currentlyDimmed) {
displayMode = 0;
lastSwitch = millis();
Serial.println(F("[DISPLAY] advanceDisplayMode(): Staying in CLOCK because Clock-only-dimming is enabled and dimming is active."));
return;
}
}
prevDisplayMode = displayMode;
int oldMode = displayMode;
String ntpField = String(ntpServer2);
@@ -2594,6 +2744,15 @@ void loop() {
P.setIntensity(targetBrightness);
}
// Enforce "Clock only during dimming" if enabled
if (clockOnlyDuringDimming && dimActive) {
if (displayMode != 0) {
prevDisplayMode = displayMode;
displayMode = 0;
lastSwitch = millis();
Serial.println(F("[DISPLAY] Forcing CLOCK because 'Clock only during dimming' is enabled and dimming is active."));
}
}
// --- IMMEDIATE COUNTDOWN FINISH TRIGGER ---
if (countdownEnabled && !countdownFinished && ntpSyncSuccessful && countdownTargetTimestamp > 0 && now_time >= countdownTargetTimestamp) {

View File

@@ -7,6 +7,9 @@ const char index_html[] PROGMEM = R"rawliteral(
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<title>ESPTimeCast Settings</title>
<style>
@@ -611,6 +614,15 @@ textarea::placeholder {
</label>
<label label style="display: flex; align-items: center; justify-content: space-between; margin-top: 0.75rem;">
<span style="margin-right: 0.5em;">Clock-Only Mode When Dimmed:</span>
<span class="toggle-switch">
<input type="checkbox" id="clockOnlyDuringDimming">
<span class="toggle-slider"></span>
</span>
</label>
<div class="form-row two-col">
<div>
<label for="dimStartTime">Start Time:</label>
@@ -798,15 +810,20 @@ window.onload = function () {
// Apply initial dimming state
setDimmingFieldsEnabled();
// Initialize Clock-only-during-dimming control (depends on dimming fields)
initClockOnlyDuringDimming(data);
// Attach listeners (mutually exclusive + API dependency)
if (apiInputEl) apiInputEl.addEventListener('input', setDimmingFieldsEnabled);
autoDimmingEl.addEventListener('change', () => {
if (autoDimmingEl.checked) dimmingEnabledEl.checked = false;
setDimmingFieldsEnabled();
clearClockOnlyIfNoDimming();
});
dimmingEnabledEl.addEventListener('change', () => {
if (dimmingEnabledEl.checked) autoDimmingEl.checked = false;
setDimmingFieldsEnabled();
clearClockOnlyIfNoDimming();
});
// Set field values from config
@@ -1325,6 +1342,48 @@ function setWeatherUnits(val) {
});
}
// --- Clock-only-during-dimming setter (no reboot) ---
function setClockOnlyDuringDimming(val) {
fetch('/set_clock_only_dimming', {
method: 'POST',
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "value=" + (val ? 1 : 0)
}).catch(e => {
console.error('Failed to set clockOnlyDuringDimming:', e);
});
}
// Initialize the checkbox from cfg and wire up immediate save (no reboot)
function initClockOnlyDuringDimming(cfg) {
const el = document.getElementById('clockOnlyDuringDimming');
if (!el) return;
// Set initial checked value from config
el.checked = !!cfg.clockOnlyDuringDimming;
// Determine whether dimming is enabled (auto OR custom)
const autoDim = (cfg.autoDimmingEnabled === true || cfg.autoDimmingEnabled === "true" || cfg.autoDimmingEnabled === 1);
const manualDim = (cfg.dimmingEnabled === true || cfg.dimmingEnabled === "true" || cfg.dimmingEnabled === 1);
// Normalize dimBrightness from config (handle "Off" or "-1" string)
let db = cfg.dimBrightness;
if (typeof db === 'string') {
if (db.toLowerCase() === 'off') db = -1;
else db = parseInt(db, 10);
}
const dimBrightnessOk = (typeof db === 'number') ? (db !== -1) : true;
// Enable only when some dimming mode is active and dimming does not fully turn display off
el.disabled = !(autoDim || manualDim) || !dimBrightnessOk;
// On change, persist immediately (no reboot)
el.addEventListener('change', function () {
const want = this.checked;
setClockOnlyDuringDimming(want);
// optimistic UI: leave checkbox as toggled; if server fails we don't roll back here
});
}
// --- Countdown Controls Logic ---
// NEW: Function to enable/disable countdown specific fields
function setCountdownFieldsEnabled(enabled) {
@@ -1360,6 +1419,8 @@ function setDimmingFieldsEnabled(enabled) {
document.getElementById('dimBrightness').disabled = !enabled;
}
async function getLocation() {
const normalize = v => {
if (v === null || v === undefined) return '';
@@ -1610,6 +1671,17 @@ function setDimmingFieldsEnabled() {
const isCustomDimmingActive = dimmingEnabled.checked;
const isDimmingActive = isAutoDimmingActive || isCustomDimmingActive; // Brightness slider logic
// --- Update Clock-only-during-dimming checkbox state (if present) ---
const clockOnlyEl = document.getElementById('clockOnlyDuringDimming');
if (clockOnlyEl) {
// Read current brightness control value (string)
const dbEl = document.getElementById('dimBrightness');
const dbVal = dbEl ? dbEl.value : null;
const dbOk = (dbVal !== null) ? !(String(dbVal).toLowerCase() === 'off' || String(dbVal) === '-1') : true;
const currentlyDimEnabled = isAutoDimmingActive || isCustomDimmingActive;
clockOnlyEl.disabled = !currentlyDimEnabled || !dbOk;
}
// BRIGHTNESS SLIDER: Enabled if EITHER mode is active.
if (dimBrightness) {
dimBrightness.disabled = !isDimmingActive;
@@ -1623,6 +1695,31 @@ function setDimmingFieldsEnabled() {
if (dimEnd) {
dimEnd.disabled = !isCustomTimeEnabled;
}
clearClockOnlyIfNoDimming();
}
// If both dimming modes are disabled, clear & persist the Clock-only-during-dimming flag
function clearClockOnlyIfNoDimming() {
const autoEl = document.getElementById('autoDimmingEnabled');
const dimEl = document.getElementById('dimmingEnabled');
const clockEl = document.getElementById('clockOnlyDuringDimming');
if (!autoEl || !dimEl || !clockEl) return;
if (!autoEl.checked && !dimEl.checked) {
// if currently checked, uncheck and persist change immediately
if (clockEl.checked) {
clockEl.checked = false;
// persist without reboot
fetch('/set_clock_only_dimming', {
method: 'POST',
body: new URLSearchParams({ value: 'false' })
}).catch(e => console.error('Failed to persist clockOnlyDuringDimming clear:', e));
}
// also ensure it's disabled in the UI
clockEl.disabled = true;
}
}
window.addEventListener('DOMContentLoaded', () => {

BIN
assets/webui8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB