Skip to content

Commit fec5cd9

Browse files
committed
feat(image_duration): implement per-image display duration settings and greatly image config page
1 parent c57d2ec commit fec5cd9

10 files changed

Lines changed: 230 additions & 63 deletions

ESP32-P4-Allsky-Display.ino

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1977,7 +1977,9 @@ void loop() {
19771977

19781978
// Only auto-cycle if not in single image refresh mode and not paused for editing
19791979
if (cyclingEnabled && imageSourceCount > 1 && !imageProcessing && !singleImageRefreshMode && !cyclingPausedForEditing) {
1980-
if (currentTime - lastCycleTime >= currentCycleInterval || lastCycleTime == 0) {
1980+
// Use per-image duration instead of global cycle interval
1981+
unsigned long currentImageDuration = configStorage.getImageDuration(currentImageIndex) * 1000; // Convert seconds to milliseconds
1982+
if (currentTime - lastCycleTime >= currentImageDuration || lastCycleTime == 0) {
19811983
shouldCycle = true;
19821984
lastCycleTime = currentTime;
19831985
Serial.println("DEBUG: Time to cycle to next image source");

build_info.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
#define BUILD_TIME __TIME__
1010

1111
// Git information (updated by compile script)
12-
#define GIT_COMMIT_HASH "cebc9ea"
13-
#define GIT_COMMIT_FULL "cebc9ea294c822c18672f6882e0596724af79cdc"
12+
#define GIT_COMMIT_HASH "c57d2ec"
13+
#define GIT_COMMIT_FULL "c57d2ec9b40f7458eabb245a109f631a5ec66b48"
1414
#define GIT_BRANCH "snd"
1515

1616
#endif // BUILD_INFO_H

config_storage.cpp

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,13 @@ void ConfigStorage::setDefaults() {
4949
for (int i = 0; i < DEFAULT_IMAGE_SOURCE_COUNT && i < 10; i++) {
5050
config.imageSources[i] = String(DEFAULT_IMAGE_SOURCES[i]);
5151
config.imageEnabled[i] = true; // All images enabled by default
52+
config.imageDurations[i] = 30; // Default 30 seconds per image
5253
}
5354
// Clear remaining slots
5455
for (int i = DEFAULT_IMAGE_SOURCE_COUNT; i < 10; i++) {
5556
config.imageSources[i] = "";
5657
config.imageEnabled[i] = true; // Default to enabled
58+
config.imageDurations[i] = 30; // Default 30 seconds per image
5759
}
5860

5961
// Initialize per-image transformation settings with global defaults
@@ -72,6 +74,7 @@ void ConfigStorage::setDefaults() {
7274
config.defaultOffsetX = DEFAULT_OFFSET_X;
7375
config.defaultOffsetY = DEFAULT_OFFSET_Y;
7476
config.defaultRotation = DEFAULT_ROTATION;
77+
config.defaultImageDuration = 30; // Default 30 seconds for new images
7578
config.backlightFreq = BACKLIGHT_FREQ;
7679
config.backlightResolution = BACKLIGHT_RESOLUTION;
7780

@@ -156,6 +159,9 @@ void ConfigStorage::loadConfig() {
156159
String enabledKey = "img_en_" + String(i);
157160
config.imageEnabled[i] =
158161
preferences.getBool(enabledKey.c_str(), true); // Default to enabled
162+
String durationKey = "img_dur_" + String(i);
163+
config.imageDurations[i] =
164+
preferences.getULong(durationKey.c_str(), 30); // Default to 30 seconds
159165
}
160166

161167
// Load per-image transform settings
@@ -187,6 +193,8 @@ void ConfigStorage::loadConfig() {
187193
preferences.getInt("def_off_y", config.defaultOffsetY);
188194
config.defaultRotation =
189195
preferences.getFloat("def_rot", config.defaultRotation);
196+
config.defaultImageDuration =
197+
preferences.getULong("def_img_dur", config.defaultImageDuration);
190198
config.backlightFreq = preferences.getInt("bl_freq", config.backlightFreq);
191199
config.backlightResolution =
192200
preferences.getInt("bl_res", config.backlightResolution);
@@ -268,6 +276,8 @@ void ConfigStorage::saveConfig() {
268276
preferences.putString(key.c_str(), config.imageSources[i]);
269277
String enabledKey = "img_en_" + String(i);
270278
preferences.putBool(enabledKey.c_str(), config.imageEnabled[i]);
279+
String durationKey = "img_dur_" + String(i);
280+
preferences.putULong(durationKey.c_str(), config.imageDurations[i]);
271281
}
272282

273283
// Save per-image transform settings
@@ -292,6 +302,7 @@ void ConfigStorage::saveConfig() {
292302
preferences.putInt("def_off_x", config.defaultOffsetX);
293303
preferences.putInt("def_off_y", config.defaultOffsetY);
294304
preferences.putFloat("def_rot", config.defaultRotation);
305+
preferences.putULong("def_img_dur", config.defaultImageDuration);
295306
preferences.putInt("bl_freq", config.backlightFreq);
296307
preferences.putInt("bl_res", config.backlightResolution);
297308

@@ -485,6 +496,12 @@ void ConfigStorage::setDefaultRotation(float rotation) {
485496
_dirty = true;
486497
}
487498
}
499+
void ConfigStorage::setDefaultImageDuration(unsigned long duration) {
500+
if (config.defaultImageDuration != duration) {
501+
config.defaultImageDuration = duration;
502+
_dirty = true;
503+
}
504+
}
488505
void ConfigStorage::setBacklightFreq(int freq) {
489506
if (config.backlightFreq != freq) {
490507
config.backlightFreq = freq;
@@ -553,6 +570,7 @@ float ConfigStorage::getDefaultScaleY() { return config.defaultScaleY; }
553570
int ConfigStorage::getDefaultOffsetX() { return config.defaultOffsetX; }
554571
int ConfigStorage::getDefaultOffsetY() { return config.defaultOffsetY; }
555572
float ConfigStorage::getDefaultRotation() { return config.defaultRotation; }
573+
unsigned long ConfigStorage::getDefaultImageDuration() { return config.defaultImageDuration; }
556574
int ConfigStorage::getBacklightFreq() { return config.backlightFreq; }
557575
int ConfigStorage::getBacklightResolution() {
558576
return config.backlightResolution;
@@ -621,6 +639,7 @@ void ConfigStorage::addImageSource(const String &url) {
621639
if (config.imageSourceCount < 10) {
622640
config.imageSources[config.imageSourceCount] = url;
623641
config.imageEnabled[config.imageSourceCount] = true; // New images enabled by default
642+
config.imageDurations[config.imageSourceCount] = config.defaultImageDuration; // Use default duration for new images
624643
config.imageSourceCount++;
625644
_dirty = true;
626645
}
@@ -997,6 +1016,22 @@ bool ConfigStorage::isImageEnabled(int index) {
9971016
return true; // Default to enabled if index out of range
9981017
}
9991018

1019+
void ConfigStorage::setImageDuration(int index, unsigned long duration) {
1020+
if (index >= 0 && index < 10) {
1021+
if (config.imageDurations[index] != duration) {
1022+
config.imageDurations[index] = duration;
1023+
_dirty = true;
1024+
}
1025+
}
1026+
}
1027+
1028+
unsigned long ConfigStorage::getImageDuration(int index) {
1029+
if (index >= 0 && index < 10) {
1030+
return config.imageDurations[index];
1031+
}
1032+
return 30; // Default to 30 seconds if index out of range
1033+
}
1034+
10001035
// Home Assistant REST Control getters
10011036
String ConfigStorage::getHABaseUrl() { return config.haBaseUrl; }
10021037
String ConfigStorage::getHAAccessToken() { return config.haAccessToken; }

config_storage.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class ConfigStorage {
4848
void setDefaultOffsetX(int offset);
4949
void setDefaultOffsetY(int offset);
5050
void setDefaultRotation(float rotation);
51+
void setDefaultImageDuration(unsigned long duration);
5152
void setBacklightFreq(int freq);
5253
void setBacklightResolution(int resolution);
5354
void setWatchdogTimeout(unsigned long timeout);
@@ -66,6 +67,8 @@ class ConfigStorage {
6667
void clearImageSources();
6768
void setImageEnabled(int index, bool enabled);
6869
bool isImageEnabled(int index);
70+
void setImageDuration(int index, unsigned long duration);
71+
unsigned long getImageDuration(int index);
6972

7073
// Individual parameter getters
7174
String getWiFiSSID();
@@ -92,6 +95,7 @@ class ConfigStorage {
9295
int getDefaultOffsetX();
9396
int getDefaultOffsetY();
9497
float getDefaultRotation();
98+
unsigned long getDefaultImageDuration();
9599
int getBacklightFreq();
96100
int getBacklightResolution();
97101
unsigned long getWatchdogTimeout();
@@ -213,6 +217,7 @@ class ConfigStorage {
213217
int imageSourceCount;
214218
String imageSources[10]; // Array of image source URLs (MAX_IMAGE_SOURCES)
215219
bool imageEnabled[10]; // Array of enabled/disabled states for each image source
220+
unsigned long imageDurations[10]; // Display duration in seconds for each image source
216221

217222
// Per-image transformation settings
218223
ImageTransform imageTransforms[10]; // Transformation settings for each image source
@@ -225,6 +230,7 @@ class ConfigStorage {
225230
int defaultOffsetX;
226231
int defaultOffsetY;
227232
float defaultRotation;
233+
unsigned long defaultImageDuration;
228234
int backlightFreq;
229235
int backlightResolution;
230236

docs/API_REFERENCE.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,6 +1054,71 @@ float getImageRotation(int index);
10541054
void copyDefaultsToImageTransform(int index);
10551055
```
10561056
1057+
#### Per-Image Duration Settings
1058+
1059+
```cpp
1060+
/**
1061+
* @brief Set display duration for specific image.
1062+
* @param index Image index (0-9)
1063+
* @param duration Display time in seconds (5-3600)
1064+
*
1065+
* Sets how long this specific image will be displayed before switching
1066+
* to the next image in the cycle.
1067+
*/
1068+
void setImageDuration(int index, unsigned long duration);
1069+
1070+
/**
1071+
* @brief Get display duration for specific image.
1072+
* @param index Image index (0-9)
1073+
* @return Duration in seconds, or default (30) if index invalid
1074+
*/
1075+
unsigned long getImageDuration(int index);
1076+
1077+
/**
1078+
* @brief Set default duration for newly added images.
1079+
* @param duration Default display time in seconds (5-3600)
1080+
*
1081+
* When new images are added via addImageSource(), they will use this
1082+
* default duration value.
1083+
*/
1084+
void setDefaultImageDuration(unsigned long duration);
1085+
1086+
/**
1087+
* @brief Get default duration for newly added images.
1088+
* @return Default duration in seconds (default: 30)
1089+
*/
1090+
unsigned long getDefaultImageDuration();
1091+
```
1092+
1093+
#### Image Enable/Disable
1094+
1095+
```cpp
1096+
/**
1097+
* @brief Enable or disable specific image in cycling order.
1098+
* @param index Image index (0-9)
1099+
* @param enabled true to include in cycle, false to skip
1100+
*
1101+
* Disabled images are not displayed during auto-cycling but remain
1102+
* in the configuration and can be re-enabled later.
1103+
*/
1104+
void setImageEnabled(int index, bool enabled);
1105+
1106+
/**
1107+
* @brief Check if image is enabled in cycling order.
1108+
* @param index Image index (0-9)
1109+
* @return true if enabled, false if disabled or index invalid
1110+
*/
1111+
bool isImageEnabled(int index);
1112+
1113+
/**
1114+
* @brief Get count of enabled images.
1115+
* @return Number of images with enabled=true
1116+
*
1117+
* Useful for determining if cycling should occur (needs >1 enabled).
1118+
*/
1119+
int getEnabledImageCount();
1120+
```
1121+
10571122
---
10581123
10591124
## SystemMonitor

web_config.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ bool WebConfig::begin(int port) {
5454
server->on("/api/toggle-image-enabled", HTTP_POST, [this]() { handleToggleImageEnabled(); });
5555
server->on("/api/select-image", HTTP_POST, [this]() { handleSelectImage(); });
5656
server->on("/api/clear-editing-state", HTTP_POST, [this]() { handleClearEditingState(); });
57+
server->on("/api/update-image-duration", HTTP_POST, [this]() { handleUpdateImageDuration(); });
5758
server->on("/api/restart", HTTP_POST, [this]() { handleRestart(); });
5859
server->on("/api/factory-reset", HTTP_POST, [this]() { handleFactoryReset(); });
5960
server->on("/api/set-log-severity", HTTP_POST, [this]() { handleSetLogSeverity(); });

web_config.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class WebConfig {
5858
void handleToggleImageEnabled();
5959
void handleSelectImage();
6060
void handleClearEditingState();
61+
void handleUpdateImageDuration();
6162
void handleRestart();
6263
void handleFactoryReset();
6364
void handleSetLogSeverity();

web_config_api.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ void WebConfig::handleSaveConfig() {
126126
unsigned long interval = value.toInt() * 1000UL;
127127
configStorage.setCycleInterval(interval);
128128
}
129+
else if (name == "default_image_duration") {
130+
unsigned long duration = value.toInt();
131+
configStorage.setDefaultImageDuration(duration);
132+
}
129133

130134
// Advanced settings
131135
else if (name == "update_interval") {
@@ -699,6 +703,32 @@ void WebConfig::handleClearEditingState() {
699703
sendResponse(200, "application/json", "{\"status\":\"success\"}");
700704
}
701705

706+
void WebConfig::handleUpdateImageDuration() {
707+
if (!server->hasArg("index") || !server->hasArg("duration")) {
708+
sendResponse(400, "application/json", "{\"status\":\"error\",\"message\":\"Missing parameters\"}");
709+
return;
710+
}
711+
712+
int index = server->arg("index").toInt();
713+
unsigned long duration = server->arg("duration").toInt();
714+
715+
if (index < 0 || index >= configStorage.getImageSourceCount()) {
716+
sendResponse(400, "application/json", "{\"status\":\"error\",\"message\":\"Invalid index\"}");
717+
return;
718+
}
719+
720+
if (duration < 5 || duration > 3600) {
721+
sendResponse(400, "application/json", "{\"status\":\"error\",\"message\":\"Duration must be between 5 and 3600 seconds\"}");
722+
return;
723+
}
724+
725+
LOG_INFO_F("[WebAPI] Updating image #%d duration to %lu seconds\n", index + 1, duration);
726+
configStorage.setImageDuration(index, duration);
727+
configStorage.saveConfig();
728+
729+
sendResponse(200, "application/json", "{\"status\":\"success\"}");
730+
}
731+
702732
void WebConfig::handleFactoryReset() {
703733
LOG_WARNING("[WebAPI] Factory reset requested via web interface");
704734
configStorage.resetToDefaults();
@@ -894,6 +924,7 @@ void WebConfig::handleGetAllInfo() {
894924

895925
if (configStorage.getCyclingEnabled()) {
896926
json += "\"cycle_interval\":" + String(configStorage.getCycleInterval()) + ",";
927+
json += "\"default_image_duration\":" + String(configStorage.getDefaultImageDuration()) + ",";
897928
json += "\"random_order\":" + String(configStorage.getRandomOrder() ? "true" : "false") + ",";
898929
json += "\"current_index\":" + String(configStorage.getCurrentImageIndex()) + ",";
899930
json += "\"source_count\":" + String(configStorage.getImageSourceCount()) + ",";

web_config_html.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ function toggleSelectAll(checked){const checkboxes=document.querySelectorAll('.i
154154
function updateBulkDeleteButton(){const checkboxes=document.querySelectorAll('.image-select-checkbox');const selected=Array.from(checkboxes).filter(cb=>cb.checked);const count=selected.length;const countSpan=document.getElementById('selectedCount');const bulkBtn=document.getElementById('bulkDeleteBtn');const selectAllCb=document.getElementById('selectAllImages');if(countSpan)countSpan.textContent=count;if(bulkBtn)bulkBtn.style.display=count>0?'inline-flex':'none';if(selectAllCb)selectAllCb.checked=(count>0&&count===checkboxes.length)}
155155
function bulkDeleteSelected(btn){const checkboxes=document.querySelectorAll('.image-select-checkbox');const selected=Array.from(checkboxes).filter(cb=>cb.checked);const indices=selected.map(cb=>parseInt(cb.getAttribute('data-index')));if(indices.length===0){showToast('No images selected','warning');return}const total=checkboxes.length;if(indices.length===total){showToast('Cannot delete all sources. At least one must remain.','error');return}showConfirmModal('🗑️ Delete Selected Images','Are you sure you want to delete '+indices.length+' selected image source(s)?',()=>{showButtonFeedback(btn,'loading','Deleting...');const formData=new FormData();formData.append('indices',JSON.stringify(indices));fetch('/api/bulk-delete-sources',{method:'POST',body:formData}).then(response=>response.json()).then(data=>{if(data.status==='success'){showButtonFeedback(btn,'success','Deleted!');showToast(data.message||'Selected sources deleted','success');setTimeout(()=>location.reload(),1000)}else{showButtonFeedback(btn,'error','Error');showToast('Error: '+data.message,'error')}}).catch(error=>{showButtonFeedback(btn,'error','Failed');showToast('Network error','error')})})}
156156
function toggleImageEnabled(index,btn){const formData=new FormData();formData.append('index',index);fetch('/api/toggle-image-enabled',{method:'POST',body:formData}).then(response=>response.json()).then(data=>{if(data.status==='success'){showToast(data.enabled?'Image enabled':'Image disabled','success');setTimeout(()=>location.reload(),500)}else{showToast('Failed to toggle image state','error')}}).catch(error=>{showToast('Network error','error')})}
157+
function updateImageDuration(index,input){const duration=parseInt(input.value);if(duration<5||duration>3600){showToast('Duration must be between 5 and 3600 seconds','error');return}const formData=new FormData();formData.append('index',index);formData.append('duration',duration);fetch('/api/update-image-duration',{method:'POST',body:formData}).then(response=>response.json()).then(data=>{if(data.status==='success'){showToast('Duration updated','success')}else{showToast('Failed to update duration','error')}}).catch(error=>{showToast('Network error','error')})}
157158
function selectImageForEditing(index,btn){showButtonFeedback(btn,'loading','Loading...');const formData=new FormData();formData.append('index',index);fetch('/api/select-image',{method:'POST',body:formData}).then(response=>response.json()).then(data=>{if(data.status==='success'){showToast('Switched to image #'+(index+1),'success');if(typeof resetCycleTimer==='function')resetCycleTimer();location.reload()}else{showToast('Failed to switch image','error');showButtonFeedback(btn,'error','Error')}}).catch(error=>{showToast('Network error','error');showButtonFeedback(btn,'error','Failed')})}
158159
function updateSelectedImageTransform(property,value){const index=parseInt(document.getElementById('selectedImageNumber').textContent)-1;const formData=new FormData();formData.append('index',index);formData.append('property',property);formData.append('value',value);fetch('/api/update-transform',{method:'POST',body:formData}).then(response=>response.json()).then(data=>{if(data.status!=='success'){showToast('Failed to update '+property,'error')}else{if(typeof resetCycleTimer==='function')resetCycleTimer()}}).catch(error=>{showToast('Network error','error')})}
159160
function copyDefaultsToSelectedImage(btn){const index=parseInt(document.getElementById('selectedImageNumber').textContent)-1;showConfirmModal('📋 Copy Global Defaults','Copy global default transformation settings to Image #'+(index+1)+'?',()=>{showButtonFeedback(btn,'loading','Copying...');const formData=new FormData();formData.append('index',index);fetch('/api/copy-defaults',{method:'POST',body:formData}).then(response=>response.json()).then(data=>{if(data.status==='success'){showButtonFeedback(btn,'success','Copied!');showToast('Defaults copied','success');setTimeout(()=>location.reload(),1000)}else{showButtonFeedback(btn,'error','Error');showToast('Failed to copy defaults','error')}}).catch(error=>{showButtonFeedback(btn,'error','Failed');showToast('Network error','error')})})}

0 commit comments

Comments
 (0)