Skip to content

Commit 0b3d5ef

Browse files
committed
update firmware v1.0.1
1 parent 2b44d28 commit 0b3d5ef

11 files changed

Lines changed: 325 additions & 91 deletions

File tree

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
# CMakeLists in this exact order for cmake to work correctly
33
cmake_minimum_required(VERSION 3.16)
44

5+
set(PROJECT_VER "1.0.1")
6+
57
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
68
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
79

main/apps/app_manager/app_manager.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -689,7 +689,7 @@ static void app_task(void* param)
689689
// ---- Startup ----
690690
esp_err_t app_manager_start()
691691
{
692-
ESP_LOGI(g_tag, "Software version: V%u", APP_SW_VERSION);
692+
ESP_LOGI(g_tag, "Software version: V%s", APP_SW_VERSION);
693693

694694
// ── First boot guide image ──
695695
nvs_handle_t h;

main/apps/app_manager/app_manager.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
#define WIFI_AP_AUTO_OFF_TIMEOUT_MIN (10)
2222

2323
/** @brief Current application software version. */
24-
#define APP_SW_VERSION (0x01)
24+
#define APP_SW_VERSION "1.0.1"
2525

2626
/**
2727
* @brief Displays the boot guide image.

main/apps/app_server/app_server.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ extern const char _binary_index_html_end[] asm("_binary_index_html_end");
5454

5555
#define SCAN_TIMEOUT_MS (8000)
5656
#define CONNECT_TIMEOUT_MS (15000)
57-
#define UPLOAD_MAX_SIZE (512 * 1024)
57+
#define UPLOAD_MAX_SIZE (2 * 1024 * 1024)
5858
#define PHOTOS_PER_PAGE (16)
5959
#define MAX_PHOTOS (500)
6060

@@ -138,7 +138,8 @@ static bool url_encode_component(const char *src, char *dst, size_t dst_sz)
138138

139139
for (size_t i = 0; src[i]; i++) {
140140
unsigned char c = (unsigned char)src[i];
141-
bool safe = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' ||
141+
142+
bool safe = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' ||
142143
c == '_' || c == '.' || c == '~';
143144

144145
if (safe) {

main/apps/app_server/index.html

Lines changed: 88 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1592,6 +1592,10 @@ <h1 class="page-title">EzData Push</h1>
15921592
let confirmCallback = null;
15931593
let photoPage = 1;
15941594
let photoTotalPages = 1;
1595+
let photoPageCache = {}; // { [page]: { total_pages, names: [...] } }
1596+
let photoCache = {}; // { [name]: { name, url } }
1597+
let photoTotal = 0;
1598+
let photoPageLoading = false;
15951599

15961600
// ==================== API Helper ====================
15971601
async function apiCall(method, url, body, isFormData) {
@@ -1600,15 +1604,23 @@ <h1 class="page-title">EzData Push</h1>
16001604
opts.body = isFormData ? body : JSON.stringify(body);
16011605
if (!isFormData) opts.headers = { "Content-Type": "application/json" };
16021606
}
1607+
let res;
16031608
try {
1604-
const res = await fetch(url, opts);
1605-
const data = await res.json();
1606-
if (!res.ok) throw new Error(data.message || `HTTP ${res.status}`);
1607-
return data;
1609+
res = await fetch(url, opts);
16081610
} catch (e) {
1609-
if (e.message && !e.message.startsWith("HTTP")) throw e;
1611+
throw e;
1612+
}
1613+
1614+
let data = null;
1615+
try {
1616+
data = await res.json();
1617+
} catch (e) {
1618+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
16101619
throw new Error("Network request failed");
16111620
}
1621+
1622+
if (!res.ok) throw new Error(data.message || `HTTP ${res.status}`);
1623+
return data;
16121624
}
16131625

16141626
// ==================== Toast ====================
@@ -2654,11 +2666,6 @@ <h1 class="page-title">EzData Push</h1>
26542666
requestAnimationFrame(() => {
26552667
rerenderCompositionLayout(false);
26562668
});
2657-
2658-
if (currentMode === "mode_1") {
2659-
photoPage = 1;
2660-
loadPhotoList(photoPage);
2661-
}
26622669
}
26632670

26642671
// ==================== Unified Settings Modal ====================
@@ -3271,10 +3278,17 @@ <h1 class="page-title">EzData Push</h1>
32713278
formData.append("action", "upload_only");
32723279
formData.append("algorithm", displayMode);
32733280
showToast("Uploading...", "");
3274-
await apiCall("POST", "/api/photos/upload", formData, true);
3281+
const resp = await apiCall("POST", "/api/photos/upload", formData, true);
32753282
showToast("Uploaded successfully", "success");
3276-
photoPage = 9999;
3277-
loadPhotoList(photoPage);
3283+
if (displayMode === "nearest" && resp && resp.name) {
3284+
// Nearest photo inserts at front — all page boundaries shift, clear page cache
3285+
photoPageCache = {};
3286+
const m = resp.name.match(/^imageN0*(\d+)/i);
3287+
if (m) photoPage = Math.ceil(Number(m[1]) / getPhotoPerPage());
3288+
} else {
3289+
photoPage = 9999;
3290+
}
3291+
await loadPhotoList(photoPage, /* forceRefresh */ true);
32783292
loadStorage();
32793293
} catch(e) { showToast(e.message, "error"); }
32803294
}
@@ -3300,10 +3314,18 @@ <h1 class="page-title">EzData Push</h1>
33003314
formData.append("algorithm", displayMode);
33013315

33023316
showToast("Uploading...", "");
3303-
await apiCall("POST", "/api/photos/upload", formData, true);
3317+
const resp = await apiCall("POST", "/api/photos/upload", formData, true);
33043318
showToast("Uploaded & display updated", "success");
3305-
photoPage = 9999;
3306-
loadPhotoList(photoPage);
3319+
// Compute which page the new photo landed on from its name
3320+
if (displayMode === "nearest" && resp && resp.name) {
3321+
// Nearest photo inserts at front — all pages shift, clear page cache
3322+
photoPageCache = {};
3323+
const m = resp.name.match(/^imageN0*(\d+)/i);
3324+
if (m) photoPage = Math.ceil(Number(m[1]) / getPhotoPerPage());
3325+
} else {
3326+
photoPage = 9999; // Dither appends at end, only last page changes
3327+
}
3328+
await loadPhotoList(photoPage, /* forceRefresh */ true);
33073329
loadStorage();
33083330
} catch(e) { showToast(e.message, "error"); }
33093331
}
@@ -3315,20 +3337,43 @@ <h1 class="page-title">EzData Push</h1>
33153337
return 12;
33163338
}
33173339

3318-
async function loadPhotoList(page) {
3340+
async function loadPhotoList(page, forceRefresh = false) {
33193341
page = page || photoPage;
33203342
const perPage = getPhotoPerPage();
3343+
3344+
// Serve from cache if this page was loaded before
3345+
if (!forceRefresh && photoPageCache[page]) {
3346+
photoPage = page;
3347+
renderPhotoGridFromNames(photoPageCache[page]);
3348+
renderPagination();
3349+
return;
3350+
}
3351+
3352+
photoPageLoading = true;
33213353
try {
33223354
const data = await apiCall("GET", `/api/photos/list?page=${page}&per_page=${perPage}`);
3323-
photoPage = data.page;
3324-
photoTotalPages = data.total_pages;
3325-
renderPhotoGrid(data.photos, data.total);
3326-
renderPagination();
3355+
if (data.page === page || page === photoPage) {
3356+
// Store per-photo data globally
3357+
const names = [];
3358+
data.photos.forEach(p => {
3359+
photoCache[p.name] = { name: p.name, url: p.url };
3360+
names.push(p.name);
3361+
});
3362+
photoPageCache[data.page] = names;
3363+
photoPage = data.page;
3364+
photoTotalPages = data.total_pages;
3365+
photoTotal = data.total;
3366+
renderPhotoGridFromNames(names);
3367+
renderPagination();
3368+
}
33273369
} catch(e) { /* silent */ }
3370+
photoPageLoading = false;
33283371
}
33293372

33303373
function goPhotoPage(page) {
33313374
if (page < 1 || page > photoTotalPages) return;
3375+
photoPage = page;
3376+
renderPagination();
33323377
loadPhotoList(page);
33333378
}
33343379

@@ -3348,25 +3393,26 @@ <h1 class="page-title">EzData Push</h1>
33483393
next.disabled = photoPage >= photoTotalPages;
33493394
}
33503395

3351-
function renderPhotoGrid(photos, total) {
3396+
function renderPhotoGridFromNames(names) {
33523397
const grid = document.getElementById("photo-grid");
33533398
const count = document.getElementById("photo-count");
3354-
count.textContent = total > 0 ? `(${total})` : "";
3399+
count.textContent = photoTotal > 0 ? `(${photoTotal})` : "";
33553400

3356-
if (photos.length === 0) {
3401+
if (names.length === 0) {
33573402
grid.innerHTML = `<div style="color:#999;font-size:14px;text-align:center;grid-column:1/-1;padding:20px;">No photos</div>`;
33583403
return;
33593404
}
3360-
grid.innerHTML = photos.map(p =>
3361-
`<div class="photo-card">
3405+
grid.innerHTML = names.map(name => {
3406+
const p = photoCache[name] || { name, url: "" };
3407+
return `<div class="photo-card">
33623408
<img class="thumb" src="${escapeHtml(p.url)}" alt="${escapeHtml(p.name)}" loading="lazy">
33633409
<div class="photo-info"><div class="photo-name" title="${escapeHtml(p.name)}">${escapeHtml(p.name)}</div></div>
33643410
<div class="photo-actions">
33653411
<button class="btn btn-outline btn-sm" onclick="displayPhoto('${escapeAttr(p.name)}')">View</button>
33663412
<button class="btn btn-danger btn-sm" onclick="deletePhoto('${escapeAttr(p.name)}')">Del</button>
33673413
</div>
3368-
</div>`
3369-
).join("");
3414+
</div>`;
3415+
}).join("");
33703416
}
33713417

33723418
function escapeAttr(s) { return s.replace(/'/g, "\\'").replace(/"/g, "&quot;"); }
@@ -3385,13 +3431,23 @@ <h1 class="page-title">EzData Push</h1>
33853431
await apiCall("DELETE", "/api/photos/delete?name=" + encodeURIComponent(name));
33863432
showToast("Deleted", "success");
33873433
loadStorage();
3388-
// 如果当前页删空则回退一页
3389-
if (photoPage > 1) {
3434+
// Remove only this photo from caches
3435+
delete photoCache[name];
3436+
photoTotal = Math.max(0, photoTotal - 1);
3437+
for (const key of Object.keys(photoPageCache)) {
3438+
photoPageCache[key] = photoPageCache[key].filter(n => n !== name);
3439+
}
3440+
// If the current page is now empty, step back
3441+
const names = photoPageCache[photoPage];
3442+
if (names && names.length === 0 && photoPage > 1) {
3443+
photoPage--;
3444+
} else if (!names && photoPage > 1) {
3445+
// Page not cached — check server whether it's now empty
33903446
const perPage = getPhotoPerPage();
33913447
const data = await apiCall("GET", `/api/photos/list?page=${photoPage}&per_page=${perPage}`);
3392-
if (data.photos.length === 0 && photoPage > 1) { photoPage--; }
3448+
if (data.photos.length === 0) { photoPage--; }
33933449
}
3394-
loadPhotoList(photoPage);
3450+
loadPhotoList(photoPage, /* forceRefresh */ true);
33953451
} catch(e) { showToast(e.message, "error"); }
33963452
});
33973453
}

main/apps/local_photo_slideshow/local_photo_slideshow.cpp

Lines changed: 73 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,77 @@
2222

2323
static const char *TAG = "Slideshow";
2424

25-
// Define an invalid index to represent “no photo is currently displayed”
25+
static constexpr uint8_t RX8130_RAM_INDEX_CURRENT_STORAGE = 0;
26+
27+
// Define an invalid index to represent "no photo is currently displayed"
2628
#define NO_PHOTO 0xFFFF
2729

30+
namespace {
31+
32+
void resetPhotoIndexState(uint16_t &pending_index)
33+
{
34+
pending_index = NO_PHOTO;
35+
hal.rx8130RamWrite(RX8130_RAM_INDEX_CURRENT_STORAGE, 0);
36+
hal.rx8130RamWrite(RX8130_RAM_INDEX_CURRENT_STORAGE + 1, 0);
37+
}
38+
39+
esp_err_t selectStorageMedia(bool sd_inserted, bool &sd_fallback_locked)
40+
{
41+
sd_fallback_locked = false;
42+
43+
if (sd_inserted) {
44+
esp_err_t ret = hal_storage_init(APP_STORAGE_MEDIA_SDMMC);
45+
if (ret == ESP_OK) {
46+
return ESP_OK;
47+
}
48+
sd_fallback_locked = true;
49+
ESP_LOGW(TAG, "SD init failed, fallback to SPI flash: %s", esp_err_to_name(ret));
50+
}
51+
52+
return hal_storage_init(APP_STORAGE_MEDIA_SPIFLASH);
53+
}
54+
55+
void ensureStorageMedia(bool sd_inserted, bool &last_sd_inserted, bool &sd_fallback_locked, uint16_t &pending_index)
56+
{
57+
if (!sd_inserted) {
58+
last_sd_inserted = false;
59+
sd_fallback_locked = false;
60+
} else if (!last_sd_inserted) {
61+
last_sd_inserted = true;
62+
sd_fallback_locked = false;
63+
}
64+
65+
hal_storage_media_t target_media =
66+
(sd_inserted && !sd_fallback_locked) ? APP_STORAGE_MEDIA_SDMMC : APP_STORAGE_MEDIA_SPIFLASH;
67+
if (hal_storage_get_media() == target_media) {
68+
return;
69+
}
70+
71+
esp_err_t ret = hal_storage_switch(target_media);
72+
if (ret == ESP_OK) {
73+
resetPhotoIndexState(pending_index);
74+
return;
75+
}
76+
77+
if (!sd_inserted) {
78+
ESP_LOGW(TAG, "SPI flash switch failed: %s", esp_err_to_name(ret));
79+
return;
80+
}
81+
82+
sd_fallback_locked = true;
83+
ESP_LOGW(TAG, "SD switch failed, fallback to SPI flash: %s", esp_err_to_name(ret));
84+
if (hal_storage_get_media() != APP_STORAGE_MEDIA_SPIFLASH) {
85+
esp_err_t fallback_ret = hal_storage_switch(APP_STORAGE_MEDIA_SPIFLASH);
86+
if (fallback_ret != ESP_OK) {
87+
ESP_LOGE(TAG, "SPI flash fallback switch failed: %s", esp_err_to_name(fallback_ret));
88+
return;
89+
}
90+
}
91+
resetPhotoIndexState(pending_index);
92+
}
93+
94+
} // namespace
95+
2896
/* ---------- millis() ---------- */
2997
static inline uint32_t millis_()
3098
{
@@ -43,19 +111,15 @@ bool PhotoSlideshow::init(const char *dir_path, uint8_t interval_min)
43111
_needs_refresh = false;
44112
_last_btn_c = false;
45113
_last_btn_b = false;
114+
_last_sd_inserted = hal.isSDCardInserted();
115+
_sd_fallback_locked = false;
46116
_photo_list.clear();
47117
_scr_w = hal.Canvas->width();
48118
_scr_h = hal.Canvas->height();
49119

50120
M5.Speaker.setVolume(120);
51121

52-
if (hal.isSDCardInserted()) {
53-
// Use the SD card at startup
54-
hal_storage_init(APP_STORAGE_MEDIA_SDMMC);
55-
} else {
56-
// Use APP storage at startup
57-
hal_storage_init(APP_STORAGE_MEDIA_SPIFLASH);
58-
}
122+
selectStorageMedia(_last_sd_inserted, _sd_fallback_locked);
59123

60124
scanPhotos();
61125
hal.statusEventSend(OPERATION_EVENT_STARTUP_SUCCESS);
@@ -238,23 +302,7 @@ void PhotoSlideshow::update()
238302

239303
syncSettings();
240304

241-
// If an SD card is inserted and FLASH storage is currently in use, switch to the SD card
242-
// If no SD card is inserted and SD storage is currently in use, switch to FLASH storage
243-
if (hal.isSDCardInserted()) {
244-
if (hal_storage_get_media() != APP_STORAGE_MEDIA_SDMMC) {
245-
hal_storage_switch(APP_STORAGE_MEDIA_SDMMC);
246-
_pending_index = NO_PHOTO;
247-
hal.rx8130RamWrite(RX8130_RAM_INDEX_CURRENT, 0);
248-
hal.rx8130RamWrite(RX8130_RAM_INDEX_CURRENT + 1, 0);
249-
}
250-
} else {
251-
if (hal_storage_get_media() != APP_STORAGE_MEDIA_SPIFLASH) {
252-
hal_storage_switch(APP_STORAGE_MEDIA_SPIFLASH);
253-
_pending_index = NO_PHOTO;
254-
hal.rx8130RamWrite(RX8130_RAM_INDEX_CURRENT, 0);
255-
hal.rx8130RamWrite(RX8130_RAM_INDEX_CURRENT + 1, 0);
256-
}
257-
}
305+
ensureStorageMedia(hal.isSDCardInserted(), _last_sd_inserted, _sd_fallback_locked, _pending_index);
258306

259307
// Check buttons
260308
handleButtons();

main/apps/local_photo_slideshow/local_photo_slideshow.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ class PhotoSlideshow {
158158
bool _last_btn_c = false;
159159
bool _last_btn_b = false;
160160
bool _last_btn_a = false;
161+
bool _last_sd_inserted = false;
162+
bool _sd_fallback_locked = false;
161163

162164
/* ---- Internal helpers ---- */
163165
void syncSettings();

main/hal/hal.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ void Hal::init()
178178
s_spi_bus_inited = true;
179179
pm1.begin(&M5.In_I2C, M5PM1_DEFAULT_ADDR, M5PM1_I2C_FREQ_100K);
180180
pm1.setI2cConfig(0);
181+
pm1.pinMode(SD_DET_EN, OUTPUT);
182+
pm1.digitalWrite(SD_DET_EN, HIGH);
181183
pm1.pinMode(SD_DEC, INPUT_PULLUP);
182184
pm1.pinMode(EPD_EN, OUTPUT);
183185
pm1.digitalWrite(EPD_EN, HIGH);

0 commit comments

Comments
 (0)