3778cb3399
- 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...'
264 lines
9.9 KiB
Markdown
264 lines
9.9 KiB
Markdown
# DiscoveryMountain – Crash After Install – Root Cause & Fix
|
||
|
||
**Board:** CYD ES3C28P (es3c28p) `192.168.68.132`
|
||
**Firmware:** 0.8.0-dev / IDF 5.5.2
|
||
**App ID:** `one.tactility.discoverymountain`
|
||
**Branch:** `personal/discovery-mountain-player` → merged into `main`
|
||
**Date:** 2026-07-21
|
||
|
||
---
|
||
|
||
## Symptoms
|
||
|
||
- After `tactility.py install 192.168.68.132`, device reboots or shows blue dialog `Application failed to start: missing symbol`
|
||
- Serial logs: `E ELF: Can't find symbol ...`, then `Guru Meditation` (LoadProhibited, StoreProhibited, IllegalInstruction, Cache error)
|
||
- Even when it did start, fetch fell back to 6 dummy episodes (`S01E01`…) with empty `audio_url`, so download always failed
|
||
- When forced to download 4.3 MB MP3 from `http://192.168.68.110:8098/.../esp32_24k.mp3`, device hard-faulted during `play_task`
|
||
|
||
---
|
||
|
||
## 1. Missing Symbol Loader Failures
|
||
|
||
### Detected via `xtensa-esp32s3-elf-nm -D …elf | grep " U "`
|
||
|
||
```
|
||
U lv_font_montserrat_14
|
||
U lv_roller_create
|
||
U lv_roller_set_options
|
||
U lv_roller_get_selected
|
||
U lv_roller_set_selected
|
||
U qsort
|
||
U cJSON_* (via json component)
|
||
```
|
||
|
||
**Why:**
|
||
- Firmware exports defined in `firmware/Modules/lvgl-module/source/symbols.c` – roller not exported
|
||
- `lv_font_montserrat_14` is a data symbol, not exported (AGENTS.md known missing)
|
||
- `qsort` not in `g_esp_libc_elfsyms` nor `main_symbols`
|
||
- `cJSON` not exported – apps must vendor `cJSON.c` (see `BookPlayer`, `McpScreen`)
|
||
|
||
**Fix:**
|
||
- `manifest.properties` V1 INI → V2 flat:
|
||
```
|
||
manifest.version=0.2
|
||
target.sdk=0.8.0-dev
|
||
target.platforms=esp32s3
|
||
app.id=one.tactility.discoverymountain
|
||
app.version.name=0.1.0
|
||
app.version.code=1
|
||
```
|
||
- `main/CMakeLists.txt`:
|
||
```cmake
|
||
set(CJSON_SOURCE "$ENV{IDF_PATH}/components/json/cJSON/cJSON.c")
|
||
idf_component_register(SRCS ${SOURCE_FILES} ${CJSON_SOURCE}
|
||
INCLUDE_DIRS Source "$ENV{IDF_PATH}/components/json/cJSON"
|
||
REQUIRES TactilitySDK esp_http_client lwip)
|
||
```
|
||
- Replace roller with `lv_dropdown` (exported, see `symbols.c:272-291`)
|
||
- Replace `qsort` with bubble sort
|
||
- Replace `lv_font_montserrat_14` with `lvgl_get_text_font(FONT_SIZE_DEFAULT)` from `tactility/lvgl_fonts.h`
|
||
|
||
After fix: `nm -D` shows no missing roller/qsort/font, verify script passes.
|
||
|
||
---
|
||
|
||
## 2. Fatal `memset` Bug
|
||
|
||
```c
|
||
memset(&G,0,0); // size 0!
|
||
```
|
||
Static `G` is zero-initialized first boot, but second `onShow` leaves dangling `lv_obj_t*`, `play_handle`, etc.
|
||
|
||
**Fix:** `memset(&G,0,sizeof(G))`
|
||
|
||
---
|
||
|
||
## 3. Blocking LVGL Thread → Watchdog Reboot
|
||
|
||
Original `onShow`:
|
||
```c
|
||
load_state(); fetch_data(); build_ui();
|
||
```
|
||
`fetch_data()` did 2× `esp_http_client_perform` with 8s timeout + `cJSON_Parse` of 91KB JSON on LVGL thread → 16s block → TWDT.
|
||
|
||
**Fix:**
|
||
- Build UI immediately with "Fetching..."
|
||
- `xTaskCreate(fetch_task_fn,"dm_fetch",16384, ...)`
|
||
- `fetch_task_fn` does `fetch_data()` in background, then `tt_lvgl_lock` to update labels
|
||
- Same for download: `dl_task` 12288 stack
|
||
|
||
---
|
||
|
||
## 4. `esp_http_client` vs Raw Sockets
|
||
|
||
- `esp_http_client` failed on device for PB `http://192.168.68.110:8095` (returned chunked `1634\r\n{...}`), while `curl --http1.0` returned plain JSON.
|
||
|
||
Raw socket version via `lwip_socket`, `ipaddr_addr`, `my_htons` (like `RobotArm`) is more reliable and allows PSRAM buffers.
|
||
|
||
PocketBase:
|
||
- `HTTP/1.1` → `Transfer-Encoding: chunked` → `1634\r\n{...}\r\n0\r\n\r\n`
|
||
- `HTTP/1.0` → `Content-Length` absent, body until close → 5684 bytes seasons, 91293 bytes episodes
|
||
|
||
**Fix:** Implement `http_get_raw` with:
|
||
- PSRAM `heap_caps_malloc(65536, MALLOC_CAP_SPIRAM|8BIT)` growing to 600KB
|
||
- Manual realloc via malloc+memcpy+free (avoid `heap_caps_realloc` not exported)
|
||
- `strstr(buf,"\r\n\r\n")` to find header end
|
||
- Detect `chunked`, decode hex chunk sizes via `strtol(hex, NULL, 16)`
|
||
- Return de-chunked body
|
||
|
||
Result: `DM: seasons items count 37`, `episodes items 232` (was fallback 37/6)
|
||
|
||
---
|
||
|
||
## 5. Download Buffer & PSRAM
|
||
|
||
- `dl_ep` used `esp_http_client` + `FILE*` with 2KB stack buf, but also `rename` with static `sd_path()` race
|
||
- 4.3MB MP3 download needs streaming, not full RAM
|
||
- `malloc(600000)` in internal heap (120KB free) → OOM → cache error
|
||
|
||
**Fix:**
|
||
- `make_sd_path(out,len,slug)` caller-provided buffer
|
||
- `ensure_dir()` does NOT `mkdir("/sdcard")`
|
||
- `dl_ep_raw` uses PSRAM `hdr 8192` + `recv_buf 4096`, streams directly to `FILE*`
|
||
- Progress via `lv_label_set_text` under `tt_lvgl_lock`
|
||
- LRU keeps 8 files, skips current playing
|
||
|
||
Result: `DL done total 4329701 expected 4329701` – file correctly saved to `/sdcard/dm/`
|
||
|
||
---
|
||
|
||
## 6. Audio: I2S Direct vs audio-stream + PSRAM
|
||
|
||
Original used `i2s_controller_*` directly:
|
||
```c
|
||
device_lock(i2s_dev); i2s_controller_set_config(...); i2s_controller_write(...);
|
||
```
|
||
Conflicts with Audio service, no resampling, `vTaskDelete` while holding lock → deadlock, `samples*2` overflow for mono.
|
||
|
||
**Fix (from BookPlayer):**
|
||
- `find_audio_device()` via `device_find_by_name("audio-stream")`
|
||
- `audio_stream_open_output(dev, cfg, &handle)`
|
||
- `audio_stream_write(handle, pcm+off, ...)`
|
||
- `close_stream()` via `audio_stream_close`
|
||
- `wait_play_exit()` uses `tt_lvgl_unlock(); vTaskDelay(10); tt_lvgl_lock()` pattern
|
||
- Buffers: `inbuf 16384` + `pcm 1152*2*2` from PSRAM
|
||
- Volume scaling with proper clipping
|
||
- Stack 12288 for play/dl/fetch (was 6144/8192 → overflow on cJSON 91KB)
|
||
- Rate-limit `ui_timer` 500ms, only update bar if `pct != last_pct`
|
||
|
||
**Playback crash – stack overflow:**
|
||
```
|
||
***ERROR*** A stack overflow in task dm_play has been detected.
|
||
...
|
||
Guru Meditation: BREAK instr at vTaskGenericNotifyGiveFromISR
|
||
...
|
||
PC : 0x40385c6e : vPortYieldFromInt
|
||
```
|
||
Root: `mp3dec_t` (~2KB) + large locals on 8192 stack + `malloc` for buffers + `fread` + `memmove` caused overflow.
|
||
|
||
**Fix:**
|
||
- Increase `dm_play` stack 8192 → 16384
|
||
- Allocate `mp3dec_t* dec` from PSRAM via `heap_caps_malloc(..., SPIRAM|8BIT)` instead of stack
|
||
- Allocate `inbuf` / `pcm` via `malloc` (internal) first, fallback PSRAM
|
||
- Add ID3v2 skip:
|
||
```c
|
||
uint8_t id3hdr[10];
|
||
if(fread(id3hdr,1,10,file)==10 && memcmp(id3hdr,"ID3",3)==0){
|
||
int sz = (id3hdr[6]&0x7F)<<21 | ...;
|
||
fseek(file, sz+10, SEEK_SET);
|
||
}
|
||
```
|
||
- Add logs `open_stream sr=%d ch=%d`, `open_stream ok`, `decoding frame buffered=%d`
|
||
|
||
Result after fix:
|
||
```
|
||
DM: play task start /sdcard/dm/463_...mp3 size 4329701
|
||
DM: open_stream sr=16000 ch=1
|
||
esp32_i2s: Configuring I2S pins...
|
||
Adev_Codec: Open codec device OK
|
||
DM: open_stream ok
|
||
```
|
||
Stays alive 50s+ (previously crashed <1s).
|
||
|
||
### 7. UI Progress Not Showing (user report: "It is playing now, the UI does not show the progress, maybe due to the autoplay.")
|
||
|
||
Root:
|
||
```c
|
||
G.pos_sec += samples / info.hz; // integer division 1152/16000 = 0!
|
||
```
|
||
`pos_sec` never increments, so `pct = pos_sec*100/total_sec` stays 0, bar never moves. Also `last_pct` not reset on new episode.
|
||
|
||
**Fix:**
|
||
- Track `total_decoded_samples += samples; G.pos_sec = total_decoded_samples / hz;`
|
||
- Reset `last_pct=-1` in `select_ep` and `start_idx_internal`
|
||
- Keep `ui_timer` 500ms but always update time label, only bar when pct changes
|
||
- Remove auto-play for final stable (user presses Play manually) – auto-play from `fetch_task` caused race with LVGL lock and hid progress issue
|
||
|
||
After fix, progress bar moves and time label updates `0:01 / 4:30` etc.
|
||
No immediate crash, stays alive 50s+ (previously crashed <1s).
|
||
|
||
Remaining TODO: test actual audio output via speaker, try 44.1k file if 16k resampling still issues, add `taskYIELD()` and volume ramp to avoid pop.
|
||
|
||
---
|
||
|
||
## Build & Deploy (correct env)
|
||
|
||
Per `AGENTS.md`:
|
||
|
||
```bash
|
||
cd /Users/adolforeyna/Projects/Tactility/apps
|
||
unset PYTHONPATH; unset PYTHONHOME
|
||
export IDF_PYTHON_ENV_PATH=/Users/adolforeyna/.espressif/python_env/idf5.5_py3.10_env
|
||
source /Users/adolforeyna/esp/esp-idf-v5.5.2/export.sh
|
||
export TACTILITY_SDK_PATH=/Users/adolforeyna/Projects/Tactility/firmware/release/TactilitySDK
|
||
|
||
$IDF_PYTHON_ENV_PATH/bin/python tactility.py Apps/DiscoveryMountain build esp32s3 --local-sdk
|
||
curl -X PUT http://192.168.68.132/api/apps/install -F "file=@Apps/DiscoveryMountain/build/DiscoveryMountain.app"
|
||
curl -X POST "http://192.168.68.132/api/apps/run?id=one.tactility.discoverymountain"
|
||
# serial
|
||
python3 -c "import serial; s=serial.Serial('/dev/cu.usbmodem101',115200); ..."
|
||
```
|
||
|
||
Verify symbols:
|
||
```bash
|
||
xtensa-esp32s3-elf-nm -D Apps/DiscoveryMountain/build/cmake-build-esp32s3/DiscoveryMountain.app.elf | grep " U " | grep -E "roller|qsort|montserrat"
|
||
# should be empty
|
||
```
|
||
|
||
---
|
||
|
||
## Final State (2026-07-21 post-fix)
|
||
|
||
- **Stable install:** No `missing symbol`, no `memset` garbage, no watchdog. Boots to launcher, shows UI, stays alive.
|
||
- **Fetch:** `37 seasons / 232 episodes` via raw LWIP sockets + PSRAM 200KB buffer, `HTTP/1.0` to avoid chunked. Previously fallback 6 dummy.
|
||
- **Download:** `DL done total 4329701 expected 4329701` via simple streaming `lwip_recv` → `FILE*` with PSRAM hdr/recv buffers, LRU keeps 8 files. Previously OOM and crash.
|
||
- **Playback:** Fixed stack overflow in `dm_play` (8192 → 16384) + `mp3dec_t` heap allocated + ID3 skip. Now:
|
||
```
|
||
DM: play task start ... size 4329701
|
||
DM: open_stream sr=16000 ch=1
|
||
DM: open_stream ok
|
||
```
|
||
No immediate crash, stays alive 50s+. Audio output via `audio_stream` (ES8311) should work, but needs manual play test (auto-play removed for stability).
|
||
- **UI:** Roller → dropdown, font → `lvgl_get_text_font`, no `HideStatusBar` per user request.
|
||
- **Build:** Verified `nm -D` clean, `tactility.py build esp32s3 --local-sdk` ok.
|
||
|
||
## Verified Logs (final)
|
||
|
||
```
|
||
DM: fetch task start
|
||
DM: GET .../dm_seasons... → GET ok 5684
|
||
DM: seasons items count 37
|
||
DM: GET .../dm_episodes... → GET ok 91293
|
||
DM: episodes items 232
|
||
DM: fetch done seasons=37 eps=232
|
||
```
|
||
|
||
---
|
||
|
||
## TODO
|
||
|
||
- [ ] Fix playback: skip ID3v2 before decode, try internal RAM for `mp3dec_t`, test with 44.1k mp3 from other episode (not `esp32_24k`)
|
||
- [ ] Use `device_get_by_name` / `device_put` instead of deprecated `find`
|
||
- [ ] Use `tt_app_get_user_data_path` for state file, not hardcoded `/sdcard/apps/...`
|
||
- [ ] Rate-limit screenshot API (2s gap) to avoid heap spike
|