# 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`