feat(DM): library UI with episode list, SD status, cache, close button, stable single-file DL
- DiscoveryMountain: new UI optimized for episode list (not player anymore) - Shows episodes for current season, indicates if on SD card (green=On SD, gray=Not DL) - Per-episode action: download single via SdDownloader or play via Mp3Player - DL Missing button for missing eps in season (currently single first missing, batch reverted) - Season selector screen with cached/total counts - Top-right X close button - Seasons/episodes cached to cache.json to avoid network fetch every launch - Fixes: overlay deletion race (deferred timer), ui_timer leak causing crash on close, season selection overwritten by fetch task - Fixes: unicode ellipsis squares - SdDownloader: revert to stable single-file mode (batch array caused crashes) - Remove batch fields and array allocation, keep single url/path download - Result bundle simple success/path/bytes - Mp3Player: add resume position support (pos_sec from bundle, total_sec estimate, total_decoded_samples), return position on exit via bundle for DM to save Tested on 192.168.68.133: opens, shows S10 2/6, Play/DL buttons, close works, no crash on open/close cycles, cache shows 'Loaded from cache, refreshing...'
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
# Discovery Mountain – Download & Playback Crash Report
|
||||
|
||||
**Date:** 2026-07-25
|
||||
**Device:** 192.168.68.133 (es3c35p, /dev/cu.usbmodem1101)
|
||||
**App ID:** `one.tactility.discoverymountain` (DM) + `one.tactility.sddownloader` (SDDL) + `one.tactility.mp3player` (MP3)
|
||||
**Firmware:** 0.8.0-dev / IDF 5.5.2 / Tactility SDK 0.8.0-dev
|
||||
|
||||
---
|
||||
|
||||
## Summary – What is failing: DM or Downloader?
|
||||
|
||||
**Both, same root cause: low internal heap (3–7 KB) + unsafe task handling.**
|
||||
|
||||
- **DM fetch:** 1st launch OK (37/232), 2nd launch crashes with StoreProhibited.
|
||||
- **Downloader (SDDL):** S01E01 dummy (empty URL) causes gui stack overflow, S01E04 34 MB file causes gui overflow + phy_wakeup crash.
|
||||
|
||||
---
|
||||
|
||||
## 1. Original Download Crash (from your logs)
|
||||
|
||||
**Log:** `2026-07-24/cu.usbmodem101_21-42-13_s002.log`
|
||||
|
||||
```
|
||||
[21:43:39] DM: DL start 396_Wherever_You_Go_S07E01 url ... 36 MB
|
||||
[21:43:42] dl header done cl=36147383, wrote initial 1143, streaming
|
||||
[21:43:57] W DM: dl timeout 1/12 total=21303/36147383
|
||||
[21:43:51] wifi:bcn_timeout,ap_probe_send_start
|
||||
[21:44:05] Guru Meditation Error: Core 0 panic'ed (StoreProhibited) PC 0x4038fc5d EXCVADDR 0x05
|
||||
Backtrace: 0x4038fc5a 0x400398b5 0x4038bd9d 0x4038d7c9 0x421fdab6
|
||||
[21:44:05] WebServerService: GET /api/sysinfo
|
||||
```
|
||||
|
||||
- Download stalls at 21 KB / 36 MB, 12×15 s timeouts = 3 min blocked in `lwip_recv`
|
||||
- `onHide` did `vTaskDelay(100)` then `vTaskDelete(dl_handle)` while still blocked in recv → fd/socket/PSRAM leak → next WebServer request crashes.
|
||||
- Also `sdmmc_read_sectors: not enough mem 0x101` seen in many logs – DMA pool 32 KB exhausted.
|
||||
|
||||
**Fix applied:**
|
||||
- Timeout 15 s → 5 s, max 12 → 5 (25 s max stall)
|
||||
- `onHide` graceful wait 2 s fetch / 3 s DL, only force delete after, no leak
|
||||
- `recv_buf`/`hdr` 8192 → 4096 PSRAM, `fflush` every 8 chunks, `vTaskDelay(10)` to let SDMMC recover
|
||||
- `sin_len = sizeof(sa)`, proper `my_htons` masking
|
||||
|
||||
---
|
||||
|
||||
## 2. S01E01 – Empty URL + GUI Stack Overflow
|
||||
|
||||
**Log:** `cu.usbmodem1101_20-33-06_s008.log`
|
||||
|
||||
```
|
||||
[20:33:14] DM: connect fail 192.168.68.110:8095 → seasons fallback, episodes fallback → done 37/6
|
||||
[20:33:17] DM: Starting external SdDownloader for S01E01 -> /sdcard/dm/S01E01.mp3
|
||||
[20:33:17] SDDL: Should autostart has_url=1 has_path=1 url= path=/sdcard/dm/S01E01.mp3
|
||||
[20:33:17] xTaskCreate download_task
|
||||
***ERROR*** A stack overflow in task gui has been detected.
|
||||
Backtrace: 0x40385b29:0x3fcdb9d0 ...
|
||||
```
|
||||
|
||||
- When PB fetch fails (connect fail), DM falls back to 6 dummy episodes `S01E01`…`S01E06` with **empty `audio_url`**.
|
||||
- User presses S01E01 → DM launches SdDownloader with empty URL → `download_one_file` checks `strlen(url)<8` → `Invalid URL` → calls `update_ui_status` + `set_result_and_close(false)` which did `vTaskDelay(1500)` + `tt_app_stop()` **from download_task** → deadlock: onHide waits for download_task, download_task waits in delay, gui task overflows.
|
||||
- Also `S01E01.mp3` path exists? No, so it tries download.
|
||||
|
||||
**Fix:**
|
||||
- `try_external_downloader` now checks `strlen(audio_url)<8` → shows "No URL – fetch failed?" and returns false, no launch
|
||||
- `btn_dl_season_cb` filters `strlen>=8`
|
||||
- `SdDownloader` `set_result_and_close` no longer calls `tt_app_stop()` from download_task (just sets result, leaves app to be closed via Cancel button) → no deadlock
|
||||
- `SdDownloader` download_task stack 16384 → 8192 to save heap
|
||||
|
||||
**User note:** "That episode should be on sd card already" – real S01E01 is `463_Discovering_Discovery_Mountain_S01E01.mp3` (4.3 MB), not dummy `S01E01.mp3`. Real file exists, dummy does not.
|
||||
|
||||
---
|
||||
|
||||
## 3. S01E04 – 34 MB File, GUI Stack Overflow on DL Start
|
||||
|
||||
**Log:** `cu.usbmodem1101_20-26-11_s006.log`
|
||||
|
||||
```
|
||||
[20:28:43] DM: Starting external SdDownloader for 460_The_Lookout...S01E04 -> /sdcard/dm/460_...mp3
|
||||
[20:28:43] SDDL: Should autostart url=http://.../460_...mp3 path=/sdcard/dm/460_...mp3
|
||||
[20:28:43] xTaskCreate download_task res=1 handle=0x3fcc3f08
|
||||
|
||||
***ERROR*** A stack overflow in task gui
|
||||
```
|
||||
|
||||
- S01E04 is 34 MB (`file_size 34460594`). Download task created, but gui task overflows immediately, before any DL log.
|
||||
- Root: `lru_check` had `FE files[200]` → 200*304=60 KB on stack of download_task (12 KB stack) → overflow. Also `sort_eps` had `EpInfo t` 500 bytes on stack inside double loop.
|
||||
- `ensure_parent_dir` had 512+512 stack, `req[512]`, etc.
|
||||
|
||||
**Fix:**
|
||||
- `lru_check`: `files` allocated from PSRAM via `heap_caps_malloc(200*sizeof(FE))`
|
||||
- `sort_eps`: temp `EpInfo t` allocated from PSRAM
|
||||
- `ensure_parent_dir`: rewritten without `strtok_r` (not exported) and without large stack, using manual parsing and `strncat`
|
||||
- Download task stack 16K → 8K, fetch task stays 12K, heap check before socket.
|
||||
|
||||
---
|
||||
|
||||
## 4. DM Second Launch – StoreProhibited 0x8208775e
|
||||
|
||||
**Log:** `..._17-37-00_s015.log` (latest after clean build)
|
||||
|
||||
```
|
||||
[20:54:50] fetch done seasons=37 eps=232
|
||||
[20:55:39] onShow heap before fetch: internal=50455 psram=4567656
|
||||
[20:55:39] xTaskCreate fetch_task res=1 handle=0x3fcc5110
|
||||
[20:55:39] fetch task start → seasons 723 ok
|
||||
[20:55:39] GET ...page=1 → 9875 ok → got 50
|
||||
[20:55:39] Guru StoreProhibited PC 0x4238a8c0 EXCVADDR 0x8208775e
|
||||
Backtrace: 0x4238a8bd(app) 0x420874f3(lv_timer.c:107) 0x42069754(esp_lvgl_port.c:242) 0x421fdaba(port.c:139)
|
||||
```
|
||||
|
||||
- 1st fetch after boot OK, 2nd fetch (app reopened) crashes 11 ms after first page GET, before header_end.
|
||||
- Backtrace shows crash in **LVGL timer task**, not fetch task: `lv_timer.c:107` → `ui_timer`
|
||||
- `ui_timer` (200 ms) was doing heavy work: `save_current_ep_pos()` + `save_state()` (cJSON create, print, fopen, fwrite) every 5 sec when playing. Called from GUI task with limited stack → StoreProhibited when trying to write to flash 0x82... (string literal in flash?).
|
||||
- Also `G` global is 240 KB (400*492) BSS in DRAM, plus `saved_pos` 40 KB = 280 KB, leaving free heap 50 KB → after first fetch cJSON 10 KB nodes, heap fragments to 3-7 KB → `MemoryChecker: Internal memory low: 3607/7279/7435`
|
||||
|
||||
**Fix:**
|
||||
- `ui_timer`: early return if `G.fetching`, remove periodic `save_state()` (saving is done explicitly in `select_ep`, `go_next`, `onHide`, `onResult`)
|
||||
- `AppCtx.MAX_EPS` 400→250, `title` 128→64, `audio_url` 256→128, `MP3_BUF` 16384→4096 → size 240 KB → ~100 KB
|
||||
- `fetch_task` stack 16384→12288, `onShow` heap log, `xTaskCreate` result log
|
||||
- Pagination 100/page → 50/page (now 25/page) with 300-400 ms delay between pages to let heap recover, `fields=season_num` and `fields=title,slug,season_num,episode_num,audio_url` to cut JSON 91 KB → 42 KB → 9 KB/page
|
||||
|
||||
---
|
||||
|
||||
## 5. DL Season Button – IntegerDivideByZero in Bundle
|
||||
|
||||
**Log:** `cu.usbmodem1101_16-57-59_s014.log`
|
||||
|
||||
```
|
||||
[17:34:06] episodes page 1 got 50 items
|
||||
[17:34:06] Guru IntegerDivideByZero PC 0x42014cf3
|
||||
Backtrace: 0x42014cf0(hashtable_policy.h:534) 0x42014dac(unordered_map.h:988) 0x42014345(tt_bundle.cpp:46 putInt32) 0x4237a4d3(app)
|
||||
```
|
||||
|
||||
- Pressing **DL Season** during fetch → `G.ep_cnt` only 50 (partial), `G.fetching` true, but button didn't check fetching → `btn_dl_season_cb` called `tt_bundle_alloc` + 20× `putString` (url+path) while internal heap low from cJSON → `unordered_map` bucket_count=0 → `% bucket_count` → divide by zero.
|
||||
|
||||
**Fix:**
|
||||
- `btn_seasons_cb` and `btn_dl_season_cb` now check `if(G.fetching) return;`
|
||||
- `btn_dl_season_cb`: `missing_idx[50]` → `[10]` to avoid LVGL stack overflow, heap check `>15 KB` before alloc, and per-put heap check `>8 KB` else abort with "Low memory, try 1 by 1"
|
||||
- Also filters `strlen(audio_url)>=8`
|
||||
|
||||
---
|
||||
|
||||
## 6. Play Button – Now Opens Mp3Player, Returns Timestamp
|
||||
|
||||
**Previous:** DM had internal `play_task` using `audio_stream` directly. User reported "pres play nothing happened" because audio-stream device not found when launched from DM (DM still held it).
|
||||
|
||||
**Fix in `player.c`:**
|
||||
```c
|
||||
stop_playback_sync(); wait_play_exit(); close_overlay();
|
||||
BundleHandle b = tt_bundle_alloc();
|
||||
putString("file", fpath); putInt32("pos_sec", saved);
|
||||
tt_app_start_with_bundle("one.tactility.mp3player", b);
|
||||
```
|
||||
- `onResult` in DM now handles Mp3Player return:
|
||||
- `optInt32("pos_sec"/"timestamp")` → `set_saved_pos_for_slug`
|
||||
- Updates UI `S01 E02 * • 0:07`, `Resume – tap Play`
|
||||
- No autoplay after DL or after Mp3Player (per request)
|
||||
|
||||
**Mp3Player (`one.tactility.mp3player`) fixes:**
|
||||
- Retry finding audio device 5×200 ms (was failing when launched from DM)
|
||||
- Tracks `pos_sec = total_decoded_samples / hz`, `total_sec` estimate
|
||||
- `onHide` returns bundle `pos_sec`, `timestamp`, `file`, `finished`, `success`
|
||||
- `onHide` now closes audio stream **before** alloc result to free heap, frees buffers before bundle, only puts 2 ints to avoid OOM, no SD file write in low heap
|
||||
|
||||
**Tested on 133:**
|
||||
```
|
||||
[20:03:30] DM: Launching Mp3Player for /sdcard/dm/463_...mp3 pos 4
|
||||
[20:03:31] Mp3Player: Found audio-stream device 0x3fca480c
|
||||
[20:03:31] Starting MP3 playback size 4329701 resume=4
|
||||
[20:03:31] Audio stream opened 16000 Hz, 1 ch
|
||||
[20:03:56] onHide start pos_sec=15
|
||||
[20:03:59] Set result bundle pos_sec=15
|
||||
[20:03:59] onHide returning pos_sec=15 total_sec=269
|
||||
[20:03:59] DM: onResult result=1 pending_idx=0 cur_idx=0 → saved pos 15
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. SdDownloader – Now Batch + Headless (per your latest spec)
|
||||
|
||||
**Manifest:** `one.tactility.sddownloader`, 30 KB, 0 missing symbols
|
||||
|
||||
**UI:** Only bar, `% (3/10)`, detail `x / y KB`, URL/path labels read-only, status, Cancel. No input fields.
|
||||
|
||||
**Args via bundle (no manual input):**
|
||||
- Single: `url`, `path`, `override` bool (default false → fail if exists)
|
||||
- Batch (full season): `count` int, `url_0`/`path_0` … `url_n`/`path_n`, `override` bool
|
||||
- Example from DM `btn_dl_season_cb`:
|
||||
```c
|
||||
bundle.putInt32("count", 7);
|
||||
putString("url_0", "http://.../S02E01.mp3"); putString("path_0", "/sdcard/dm/...");
|
||||
...
|
||||
tt_app_start_with_bundle("one.tactility.sddownloader", b);
|
||||
```
|
||||
|
||||
**Returns bool:**
|
||||
```c
|
||||
bundle.putBool("success", true/false);
|
||||
putInt32("success_count"), "fail_count", "total_count", "bytes", "path", "error"
|
||||
setResult(OK/ERROR, bundle)
|
||||
```
|
||||
DM `onResult` shows `DL: X ok, Y fail` and refreshes `seen` flags.
|
||||
|
||||
**Robustness:**
|
||||
- 8 KB stack (was 16 KB), 4 KB PSRAM buf, 15 s timeout, 5 retries
|
||||
- `ensure_parent_dir` mkdir -p without `strtok` (not exported)
|
||||
- `stat` check for exists → fail if not override
|
||||
- No `tt_app_stop()` from download task (caused deadlock + gui stack overflow for S01E01 empty URL)
|
||||
|
||||
---
|
||||
|
||||
## Current Builds on 133
|
||||
|
||||
- `DiscoveryMountain.app` 80 KB (was 70 KB) – entry 0x4237543c, 118 undef, 0 missing – 37/232 fetch, seasons dropdown works, no autoplay, DL Season 10 max, external Mp3Player/SdDownloader, no fullscreen covering (root now child of parent, overlays closed before launch)
|
||||
- `SdDownloader.app` 30 KB – 74 undef, 0 missing – batch 10, file-exists check
|
||||
- `Mp3Player.app` 40 KB – 0 missing after adding `esp_http_client` to REQUIRES – returns timestamp
|
||||
|
||||
---
|
||||
|
||||
## Still Open / Next
|
||||
|
||||
- Second fetch after app restart still StoreProhibited at 0x4238a8c0 (PC in app) during first page GET – likely `cJSON_Parse` + `has_slug` stat + low heap after first fetch. Mitigation: reduce `MAX_EPS` further or move `G` to PSRAM with `EXT_RAM_ATTR` (tried, caused other crash), or free `G.eps` between fetches.
|
||||
- S01E01 dummy fallback (empty URL) now shows "No URL – fetch failed?" instead of crash, but ideally should never appear if fetch succeeds. Ensure PB at `192.168.68.110:8095` is reachable (seen `connect fail` when WiFi bcn_timeout).
|
||||
- Mp3Player close takes 2.5 s (force delete after 50 tries) – should be optimized to not need force delete.
|
||||
|
||||
---
|
||||
|
||||
## Reproduction Steps for S01E04 Crash (you reported)
|
||||
|
||||
1. Clean fetch fails → fallback 37/6 dummy episodes `S01E01`…`S01E06` with empty URL
|
||||
2. Press S01E01 → DM tries external DL with empty URL → SDDL `url=` empty → `Invalid URL` → `set_result_and_close` did `tt_app_stop()` from download task → deadlock → gui stack overflow
|
||||
|
||||
Fixed by: empty URL check in `try_external_downloader` + `btn_dl_season_cb` + removing `tt_app_stop()` from download task.
|
||||
|
||||
For real S01E04 (34 MB, `460_...`), previous crash was gui stack overflow right after `xTaskCreate` – fixed by reducing download_task stack 16K→8K and moving `files[200]` 60KB array from stack to PSRAM in `lru_check`.
|
||||
|
||||
---
|
||||
|
||||
## Files Changed
|
||||
|
||||
- `Apps/DiscoveryMountain/main/Source/app_context.c` – added `pending_dl_idx`, `root`
|
||||
- `app_context.h` – reduced `MAX_EPS` 400→250, `MP3_BUF` 16K→4K, `title` 128→64, `audio_url` 256→128, added `root`, `pending_dl_idx`, batch fields in SDDL
|
||||
- `network.c` – 50/page pagination (was 400), fields filter, 8K internal buf, detailed logs for connect
|
||||
- `storage.c` – `sort_eps` temp from PSRAM, `lru_check` files from PSRAM (was 60KB stack)
|
||||
- `download.c` – stubbed to only overlays (internal DL removed)
|
||||
- `player.c` – simplified to only external launchers, no internal `play_task`, added `download_season()`
|
||||
- `ui.c` – `build_ui(parent)` uses parent, not `scr_act()`, root stored, 2 buttons 48% (Seasons + DL Season), `ui_timer` early return if fetching, `btn_dl_season_cb` with heap checks
|
||||
- `main.c` – onShow heap log, xTaskCreate 12288, onResult handles both Mp3Player timestamp and SdDownloader batch, onHide does NOT clear `pending_dl_idx`, deletes root
|
||||
- `SdDownloader/main/Source/main.c` – new app, batch support, file-exists + override logic, returns bool
|
||||
- `Mp3Player/main/Source/main.c` – returns `pos_sec`/`timestamp`, retry audio device, auto-finds test file if no bundle
|
||||
|
||||
---
|
||||
|
||||
## How to Test Full Season Download
|
||||
|
||||
1. Open DM → wait `fetch done 37/232`
|
||||
2. Tap `Seasons` → S01 → tap `DL Season`
|
||||
3. Should show `Starting DL 6 eps S01` → launches SDDL → bar shows `3/6: 45%` etc.
|
||||
4. On finish, DM shows `DL: 6 ok, 0 fail` and refreshes `*` markers
|
||||
5. Tap episode with `*` → Play → launches Mp3Player → plays from saved pos → close → DM shows `Resume • 0:12`
|
||||
Reference in New Issue
Block a user