Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e0c5cbc1a0 | |||
| 74fe10f130 | |||
| ca1202345e | |||
| 4e4e815995 | |||
| 279893eed0 | |||
| 1a1e391a14 | |||
| 88870d7ac5 | |||
| 827fdb41c2 | |||
| 709444d771 | |||
| 1999d2c510 | |||
| d4a257a01c | |||
| 7ef8e272fc | |||
| eb364cb34f | |||
| eaccebc95d | |||
| 3778cb3399 | |||
| 6a1fe5dda6 | |||
| d06f8ddbb5 | |||
| a2bb8e5132 | |||
| 9ee0599bec | |||
| c3689e82cc | |||
| 4c71c77bf6 | |||
| 0595e149d4 | |||
| 995297314d | |||
| 349034b8ba | |||
| 3085bac5f1 | |||
| b03cb2b244 | |||
| 3a2e6f3f11 | |||
| 9def43b4ed | |||
| c258aebca1 | |||
| 4a153099f9 | |||
| aebee50d25 | |||
| 6d9001fcd7 | |||
| 653ad8902f | |||
| e4d1d35da4 | |||
| 7c3c4ad2fe | |||
| 2a45c98fb3 | |||
| 25b966a340 | |||
| b2e9046438 | |||
| f07d347fd9 | |||
| 8f46e03070 | |||
| 6eb032cb53 | |||
| 7121009d0d | |||
| 07749d86a1 | |||
| e68909d64d | |||
| d976595879 | |||
| 8721a653e7 | |||
| 694b68de1a | |||
| 71bf2631f4 | |||
| 4ab2377970 | |||
| 8a0f9ef4e4 |
@@ -0,0 +1,259 @@
|
|||||||
|
---
|
||||||
|
name: tactility-app-development
|
||||||
|
description: Use when building, compiling, deploying, or debugging Tactility OS external ELF apps (ESP32-S3/P4). Covers SDK env, missing-symbol loader errors, asset packaging, and immersive LVGL patterns.
|
||||||
|
version: 1.0.0
|
||||||
|
author: Hermes Agent
|
||||||
|
license: MIT
|
||||||
|
metadata:
|
||||||
|
hermes:
|
||||||
|
tags: [tactility, esp32, elf-loader, lvgl, esp-idf, embedded]
|
||||||
|
related_skills: [systematic-debugging]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tactility OS App Development
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Tactility loads third-party apps as dynamic ELF binaries at runtime. The host firmware exports a symbol table (`g_esp_espidf_elfsyms` / `g_customer_elfsyms` via `ESP_ELFSYM_EXPORT` / `DEFINE_MODULE_SYMBOL`). If your ELF references anything not exported, it fails at load time with `E ELF: Can't find symbol X`. The build system also requires ESP-IDF environment variables and a specific Python interpreter to avoid venv hijacking.
|
||||||
|
|
||||||
|
This skill consolidates the build-env fix, symbol hygiene, and asset/offline-data patterns discovered across sessions.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
- Building `Apps/MyApp` via `tactility.py ... build esp32s3 --local-sdk`
|
||||||
|
- Fixing `ELF: Can't find symbol` on device serial
|
||||||
|
- Packaging large offline assets (bibles, fonts, images) for constrained RAM
|
||||||
|
- Implementing immersive single-screen LVGL UI with tap-to-toggle chrome
|
||||||
|
- Handling settings/progress persistence on SD card
|
||||||
|
|
||||||
|
Don't use for:
|
||||||
|
- Simulator (`FirmwareSim`) builds (statically linked C++ `tt::app::App` subclass, different workflow)
|
||||||
|
- Board support (devicetree, display drivers) — see `tactility-board-support` project skill
|
||||||
|
|
||||||
|
## Build Workflow (Local SDK)
|
||||||
|
|
||||||
|
Always source IDF first, export SDK path, and use IDF's Python binary directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
. /Users/adolforeyna/esp/esp-idf/export.sh
|
||||||
|
export TACTILITY_SDK_PATH=/Users/adolforeyna/Projects/Tactility/firmware/release/TactilitySDK
|
||||||
|
|
||||||
|
# Use IDF python to avoid Hermes/host venv pollution (urllib3 | type error on 3.9):
|
||||||
|
/Users/adolforeyna/.espressif/python_env/idf5.3_py3.9_env/bin/python tactility.py Apps/MyApp clean
|
||||||
|
/Users/adolforeyna/.espressif/python_env/idf5.3_py3.9_env/bin/python tactility.py Apps/MyApp build esp32s3 --local-sdk --verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
Manifest requirements (`Apps/MyApp/manifest.properties`):
|
||||||
|
```properties
|
||||||
|
[manifest]
|
||||||
|
version=0.1
|
||||||
|
[target]
|
||||||
|
sdk=0.8.0-dev
|
||||||
|
platforms=esp32s3,esp32p4
|
||||||
|
[app]
|
||||||
|
id=one.tactility.myapp
|
||||||
|
versionName=1.0.0
|
||||||
|
versionCode=1
|
||||||
|
name=My App
|
||||||
|
```
|
||||||
|
|
||||||
|
CMake component (`main/CMakeLists.txt`):
|
||||||
|
```cmake
|
||||||
|
file(GLOB_RECURSE SOURCE_FILES Source/*.c)
|
||||||
|
idf_component_register(SRCS ${SOURCE_FILES} REQUIRES TactilitySDK)
|
||||||
|
# Relax noisy werrors that other apps also ignore
|
||||||
|
target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-format-truncation -Wno-unused-but-set-variable)
|
||||||
|
```
|
||||||
|
|
||||||
|
Verification: `build/*.app` (tar) contains `assets/` + ELF; `build/cmake-build-esp32s3/*.app.elf` should exist.
|
||||||
|
|
||||||
|
## Resolving Missing-Symbol Loader Errors
|
||||||
|
|
||||||
|
Tactility's ELF loader resolves via firmware symbol table. Pre-flight check before flashing:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# After IDF export.sh, run:
|
||||||
|
# xtensa-esp32s3-elf-nm -D Apps/MyApp/build/cmake-build-esp32s3/MyApp.app.elf | grep " U "
|
||||||
|
# Compare against firmware export macros ESP_ELFSYM_EXPORT / DEFINE_MODULE_SYMBOL via grep -R in firmware/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Known Missing Symbols & Workarounds
|
||||||
|
|
||||||
|
- `lv_font_montserrat_14`, `lv_font_montserrat_18` → **Not exported**. Use exported abstraction:
|
||||||
|
```c
|
||||||
|
#include "tactility/lvgl_fonts.h"
|
||||||
|
lv_obj_set_style_text_font(label, lvgl_get_text_font(FONT_SIZE_SMALL), 0);
|
||||||
|
// FONT_SIZE_SMALL/DEFAULT/LARGE mapped to board-specific sizes
|
||||||
|
```
|
||||||
|
- `lv_obj_set_style_bg_grad_color/dir/stop` → **Not exported** on 0.8.0-dev (caused `Application failed to start: missing symbol` blue OK dialog). Use solid `0x0E0E14` instead.
|
||||||
|
- `LV_OPA_95`, `LV_OPA_5`, etc. → **Not defined**. Only 0/10/20/30/40/50/60/70/80/90/100/COVER/TRANSP exist in vendored LVGL 8.x. Use `COVER` or `90`.
|
||||||
|
- `lv_arc_create`, `lv_arc_set_range/value/bg_angles`, `lv_arc_get_value` → **Not exported on 0.7.0-dev**, added in PR #496 `dff93cb6 Add lv_arc.h symbols` after 0.7 release. Device modal `Error / Application failed to start: missing symbol` with heap still healthy (~31KB free) indicates loader failure, not OOM. Fix: arc-free horizontal slide using `lv_slider_create` + swipe via `lv_indev_get_point` / `lv_indev_active` (exported since 0.6). See `references/book-browser.md`. `lv_roller` also not exported on 0.7/0.8.
|
||||||
|
- `lv_anim_init`, `lv_anim_set_var/values/duration/exec_cb/path_cb/start`, `lv_anim_path_ease_out/linear` → **Exported since 0.6** (lines 435-446). Safe for 0.7/0.8 fleet. Use for verse fade 220/300ms editorial transition. Verified no leak, heap stable 54KB on .129.
|
||||||
|
- `lv_binfont_create/destroy` → **Exported on 0.8.0-dev** (`lv_binfont_create`) but VFS path handling fragile — `tt_app_get_assets_path` returns void, check `buf[0]!=0`. Prefer **embedded C LVGL fonts** (v5 July 12): `lv_font_conv --format lvgl --lv-include lvgl.h --bpp 4 -r 32-126 -r 0x2018-0x201D` → `Source/fonts/*.c` ~40-85KB each, `LV_FONT_DECLARE`, `(lv_font_t*)&georgia_italic_26`, lives in PSRAM via `CONFIG_ELF_LOADER_LOAD_PSRAM=y`. Heap stable 47539 free. See `references/pretty-font.md`. Legacy binfont `.fnt` also bundled in `assets/fonts/` 52KB but not used by default.
|
||||||
|
- `LV_SYMBOL_STAR` → Symbols may be absent depending on LVGL symbol set. Use `LV_SYMBOL_OK`, `LV_SYMBOL_DUMMY`, or plain UTF-8.
|
||||||
|
- `lv_obj_set_style_max_width` → **Not exported on 0.7.0-dev and 0.8.0-dev** (July 16 2026 — `check_symbols.py` found 1 missing, `E ELF: Can't find symbol`). Triggers `Application failed to start: missing symbol` blue OK dialog (`/api/screenshot` returns PNG 320x240). Fix: use `LV_PCT(92)` / `LV_PCT(80)` fixed percents for side gutters, no max_width. Verified `xtensa-esp32s3-elf-nm -D` 89 undef → 0 missing after removal.
|
||||||
|
- `lv_obj_set_scrollbar_mode` → Version-dependent, not reliably exported on 0.7/0.8. Avoid — use default scroll (add `LV_OBJ_FLAG_SCROLLABLE`) and let LVGL handle it. On QVGA intro, make scrollArea scrollable and use flex column with `LV_FLEX_FLOW_COLUMN` + `pad_row 8`.
|
||||||
|
- `xTaskGetCurrentTaskHandle` → Not exported. Replace wait loops with `tt_lvgl_unlock(); vTaskDelay(10); tt_lvgl_lock(portMAX_DELAY);`
|
||||||
|
- `lv_image_set_rotation` → Use `lv_obj_set_style_transform_rotation(obj, 1800, 0)` (value 1800 = 180°)
|
||||||
|
- `cJSON`, `esp_websocket_client`, `lwip_htons` macros → Vendor sources directly or use raw `lwip_socket`/`lwip_connect` etc., which *are* exported.
|
||||||
|
- C++ lambdas in `.c` files → `auto make_btn = [](...){}` fails compile. Keep app source as pure C (no `auto`, no lambdas) or rename to `.cpp` with `extern \"C\"` shims.
|
||||||
|
- **How to bring new widgets (arc, roller, chart, etc)** → Edit `firmware/Modules/lvgl-module/source/symbols.c`, add `DEFINE_MODULE_SYMBOL(lv_arc_create)`, etc., rebuild device firmware via `python device.py es3c28p && idf.py build flash -p /dev/cu.usbmodem101`, then rebuild app SDK. Device .129 on 0.8.0-dev already has arc, but .112 0.7.0-dev does not. See session July 12 teaching how to expose more LV elements.
|
||||||
|
|
||||||
|
See `references/symbols.md` for full list.
|
||||||
|
|
||||||
|
## Asset & Offline Data Pattern
|
||||||
|
|
||||||
|
Large data (e.g., 4.8MB Zefania XML bible) must be split to keep RAM tiny. Pattern proven for BibleVerse:
|
||||||
|
|
||||||
|
- Monolith → per-book split: `assets/bible/<bnum>.bin` = concatenated NUL-terminated UTF-8 verses, `<bnum>.idx` = header `BIBK` + version u16 + count u32 + entries `{offset u32, cnum u16, vnum u16}`, `books.json` catalog.
|
||||||
|
- Biggest chunk Psalms ~215KB vs 4.1MB monolith → load only one book at a time via `tt_app_get_assets_path`.
|
||||||
|
- Hybrid fallback: try `A:` assets path, else scan `/sdcard/bibles/WEB/` same binary format for user translations.
|
||||||
|
- Always `tar tvf build/*.app | wc -l` to verify 60+ bins packaged.
|
||||||
|
|
||||||
|
See `references/offline-data.md` for converter recipe and `templates/convert_bible.py`.
|
||||||
|
|
||||||
|
## Persistence (Settings-like File)
|
||||||
|
|
||||||
|
Follow BookPlayer/EpubReader pattern:
|
||||||
|
|
||||||
|
```c
|
||||||
|
char ud_path[320]; // from tt_app_get_user_data_path(app, ud_path, sizeof)
|
||||||
|
char prog_path[320];
|
||||||
|
snprintf(prog_path, sizeof prog_path, "%s/progress.txt", ud_path); // via tt_app_get_user_data_child_path(app, "progress.txt", ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Store `bnum cnum vnum globalIdx`; write atomically on verse change. Same for `favorites.txt`.
|
||||||
|
|
||||||
|
## Immersive UI Pattern
|
||||||
|
|
||||||
|
- Single verse fullscreen centered, container `LV_OBJ_FLAG_SCROLLABLE` removed to avoid drag recalc.
|
||||||
|
- Header + bottom toolbar hidden by default (`LV_OBJ_FLAG_HIDDEN`), tap toggles.
|
||||||
|
- **Adaptive scaling**: `apply_verse_scaling()` measures `strlen(verse)` → selects `FONT_SIZE_LARGE/DEFAULT/SMALL` + line_space 4-14 + pad tightened v4 2-10 + letter_spacing. Ultra-short (<40c) gets letter_spacing 1 and breathing room. Very long (350c+ Esther 8:9) gets compact small.
|
||||||
|
- **Editorial v4/v5 (July 12)**: Warmer verse `#F2F0E8` flat no bubble, ref uppercase `EPHESIANS 1:4` tracking 2 `#9A9AA8`, gap 10 (was 14) for closeness like PSALMS 23:1 reference image, fade transition 220ms verse / 300ms ref via `lv_anim_init/set_var/values/duration/exec_cb/path_ease_out/start` (exported since 0.6, stable heap 54KB). **v5 pretty font**: Georgia Italic embedded C 26/22/20/18/16pt + regular 24 via `lv_font_conv --format lvgl --lv-include lvgl.h` → `LV_FONT_DECLARE` + resolve helpers `resolve_verse_font/book_font` fallback to `lvgl_get_text_font()`, lives in PSRAM 315KB `.rodata`, heap 47539 stable, vision confirms "serif italic editorial like PSALMS 23:1" (Habakkuk 1:17). See `references/pretty-font.md`.
|
||||||
|
- Center column `90%x80%` centered (not 88x84) to avoid bottom ref crop on 240px screens.
|
||||||
|
- Auto-advance: `lv_timer_create(timer_cb, 60000, ctx)` + `lv_timer_reset` on manual nav.
|
||||||
|
- Rate-limit label updates (store last values) to avoid audio stutter if audio plays.
|
||||||
|
- Reference: `references/immersive-ui.md`
|
||||||
|
```c
|
||||||
|
static void apply_verse_scaling(AppCtx* ctx, const char* verse) {
|
||||||
|
size_t len = strlen(verse);
|
||||||
|
enum LvglFontSize f; int line_sp, pad_v, pad_h, letter_sp=0;
|
||||||
|
if (len < 40) { f=FONT_SIZE_LARGE; line_sp=14; pad_v=32; pad_h=36; letter_sp=1; } // ultra-short
|
||||||
|
else if (len < 100) { f=FONT_SIZE_LARGE; line_sp=11; pad_v=24; pad_h=28; } // short
|
||||||
|
else if (len < 200) { f=FONT_SIZE_DEFAULT; line_sp=9; pad_v=18; pad_h=22; } // medium
|
||||||
|
else if (len < 350) { f=FONT_SIZE_SMALL; line_sp=6; pad_v=12; pad_h=18; } // long
|
||||||
|
else { f=FONT_SIZE_SMALL; line_sp=4; pad_v=8; pad_h=14; } // epic 350+
|
||||||
|
lv_obj_set_style_text_font(ctx->lbl_verse, lvgl_get_text_font(f), 0);
|
||||||
|
lv_obj_set_style_text_line_space(ctx->lbl_verse, line_sp, 0);
|
||||||
|
lv_obj_set_style_text_letter_space(ctx->lbl_verse, letter_sp, 0);
|
||||||
|
lv_obj_set_style_pad_top(ctx->lbl_verse, pad_v, 0);
|
||||||
|
lv_obj_set_style_pad_bottom(ctx->lbl_verse, pad_v, 0);
|
||||||
|
lv_obj_set_style_pad_left(ctx->lbl_verse, pad_h, 0);
|
||||||
|
lv_obj_set_style_pad_right(ctx->lbl_verse, pad_h, 0);
|
||||||
|
}
|
||||||
|
// Call after lv_label_set_text(verse)
|
||||||
|
```
|
||||||
|
Tiers tuned from real device screenshots: short verses get giant breathing room, long verses shrink + tighten.
|
||||||
|
- Reference: `references/immersive-ui.md`
|
||||||
|
|
||||||
|
### QVGA intro layout: left info + right small icon rail (PocketDungeon v5-v7 July 16-17)
|
||||||
|
|
||||||
|
For 320x240 / 320x198 usable (after statusbar) intro screens cramp when vertically stacked (title+cards+wide text buttons = clipped GOAL text, `Loot gold, survive monsters` truncated). Pattern that fixes it:
|
||||||
|
|
||||||
|
- Structure: header (title+subtitle+best compact) + `mainRow` flex row (`ROW`, `flex_grow 1`, pad 4/8 column, bottom 8) = left `leftCol` flex_grow 1 scrollable column (pad_row 8, pad_bottom 6) + right `rightRail` fixed 44px narrow column (size CONTENT height, pad_row 10, no scroll).
|
||||||
|
- Left cards iteration (v6-v7): v6 had GOAL + HOW TO PLAY (2 cards, CONTROLS merged into HOW TO PLAY line `Arrows/WASD/Swipe/Quadrant`) + right icons 38/30. User then asked: keep only GOAL + CONTROLS on intro, teach symbols via actual playable tutorial levels instead of text explanation. So v7 final: `makeCard(leftCol, "GOAL", "Reach deepest floor. Collect gold, survive."), makeCard(leftCol, "CONTROLS", "Arrows / WASD / Swipe\nTap quadrant = move\nQ / ESC = exit")` = 2 cards, airy, no HOW TO PLAY. Each card `PCT 100%` width, `SIZE_CONTENT` height, pad 9, row 3, bg `COLOR_PANEL` radius 10, labels WRAP width 100%.
|
||||||
|
- Right icons: small icons on right as user requested — replace text `ENTER DUNGEON` button with icons: play 38px accent bg `COLOR_ACCENT` radius `s/3`, close/exit 30px muted bg `0x252836`, both `lv_btn_create` + centered label `LV_SYMBOL_PLAY` / `LV_SYMBOL_CLOSE`. No text hint below (was clipped). Saves vertical space, gives 40-50px more for cards.
|
||||||
|
- Verification via `/api/screenshot`: `curl -s http://<ip>/api/screenshot -o /tmp/intro.png` then vision check for clipping/padding. Screenshot shows 6.2-6.6KB PNG, header Best F3 G18, left 75% cards, right 25% icons. If bottom clipped, reduce cards to 2, shrink rail to 40px, icons 32/26.
|
||||||
|
- Symbol hygiene: no `lv_obj_set_style_max_width` (unexported, causes `missing symbol` blue OK dialog), no `lv_obj_set_scrollbar_mode` — use `PCT 92/80/70` for gutters and default scroll via `LV_OBJ_FLAG_SCROLLABLE`.
|
||||||
|
|
||||||
|
See `references/intro-fullscreen.md` v5-v7 and `references/sdk-mismatch-missing-symbol.md` for SDK 0.8.0-dev bump (board .112 OS 0.8.0-dev vs manifest 0.7.0-dev) and `lv_obj_set_style_max_width` removal (89 undef → 0 missing).
|
||||||
|
|
||||||
|
### Tutorial level pattern: 2-row teach-by-play (PocketDungeon v7 July 17)
|
||||||
|
|
||||||
|
User wanted: instead of text explanation in intro, have 3 intro playable levels — first presents player and stairs, next bat and gold, both with 2 rows regular cols, then go current first level. Implementation:
|
||||||
|
|
||||||
|
- `DungeonModel.h`: `TUTORIAL_ROWS=2`, `renderRows` field in `DungeonState`, `floor 0,1 = tutorial`, `displayFloor()` maps internal floor to user-visible (tutorial → 1), `isTutorial()`, `tutorialId()`, `setBests`, `reset(seed, tutorials=true)`, `generateTutorialFloor(tId)`, `clearAllTiles()`.
|
||||||
|
- `DungeonModel.cpp`: `reset` sets floor 0 if tutorials else 2 (first real = 2 internally → display 1), `clearAllTiles` border walls, `generateTutorialFloor` sets `renderRows=TUTORIAL_ROWS`, hides rows >=3 via walls, player {1,1}, Tut0: Stairs at {COLS-2,1}, Tut1: Treasure {3,1} + Stairs + Bat {5,1} hp1, no random walls, `moveMonsters` skipped when `isTutorial()`, best update only floor>=2.
|
||||||
|
- `DungeonRenderer.cpp`: respects `state.renderRows`, hides cells y>=renderRows via `HIDDEN` flag, re-centers board for tutorial `set_height grid.cellPx*TUTORIAL_ROWS align CENTER 0,-10`, status shows `TUTORIAL X/2 HP Gold`, message `TUT 1: Move to > @=you` / `TUT 2: Kill b, take $, use >` vs normal `Arrows/Swipe/Tap`.
|
||||||
|
- `PocketDungeon.cpp` intro final per user July 17: only GOAL + CONTROLS cards (2), symbols taught via play, not text.
|
||||||
|
- Verification: hermes-verify-tutorial*.py checks 2 cards, no HOW TO PLAY, tutorial gen Bat+Treasure+Stairs, renderRows, `TUTORIAL_ROWS`, isTutorial, build package ok, nm 0 missing.
|
||||||
|
- Result: flow Intro (Goal+Controls left, icons right) → Tut1 2×9 @→> → Tut2 2×9 @+b+$→> → Real game F1 infinite (display floor). Installed to 192.168.68.112, screenshots intro_tutorial.png 6.3K.
|
||||||
|
|
||||||
|
See `references/pocket-dungeon.md` tutorial update and `references/intro-fullscreen.md` v7.
|
||||||
|
|
||||||
|
## Deployment & Running
|
||||||
|
|
||||||
|
Old docs say `POST :6666/app/install`. Firmware 0.7.0-dev+ dashboard moved to port 80:
|
||||||
|
|
||||||
|
- `PUT /api/apps/install` multipart `file` field (not POST, not :6666)
|
||||||
|
- `POST /api/apps/run?id=<appId>`, `GET /api/apps`
|
||||||
|
|
||||||
|
`tactility.py` tool v3.5.1 still uses :6666 and needs Developer mode enabled on device. If port 6666 refuses, use dashboard API directly or enable dev server in Settings. Always pass `esp32s3` platform arg to `install` unless you built all platforms.
|
||||||
|
|
||||||
|
Full recipe + discovery in `references/deploy.md`.
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
1. **Hermes venv hijacks IDF Python — two flavors**
|
||||||
|
- **Flavor A (tactility.py)**: `requests` import fails with `TypeError: unsupported operand type(s) for |: 'type' and 'type'` on Python 3.9 because `PYTHONPATH` includes Hermes venv's `urllib3` using Python 3.10+ union syntax.
|
||||||
|
- **Flavor B (idf.py build/flash)**: `Cannot import module "pydantic_core._pydantic_core"` / `ModuleNotFoundError` — Hermes 3.11 venv's `pydantic_core/*.so` shadows IDF's 3.9/3.10 venv. Even `idf.py --version` fails.
|
||||||
|
- **Fix both**: clear `PYTHONPATH=""` and explicitly set `IDF_PYTHON_ENV_PATH=/Users/adolforeyna/.espressif/python_env/idf5.3_py3.9_env` before sourcing export.sh. Also:
|
||||||
|
```bash
|
||||||
|
unset PYTHONPATH; unset PYTHONHOME
|
||||||
|
export IDF_PYTHON_ENV_PATH=/Users/adolforeyna/.espressif/python_env/idf5.3_py3.9_env
|
||||||
|
. /Users/adolforeyna/esp/esp-idf/export.sh
|
||||||
|
```
|
||||||
|
- Stale Python version: If project was configured with `idf5.3_py3.10_env` but you now use 3.9 env → `idf.py fullclean` required or you get "project was configured with .../idf5.3_py3.10_env/bin/python". Do fullclean then re-generate `python device.py <id>`.
|
||||||
|
- Verification:
|
||||||
|
```bash
|
||||||
|
$IDF_PYTHON_ENV_PATH/bin/python -c "import pydantic_core; print('ok')"
|
||||||
|
env -u PYTHONPATH -u PYTHONHOME idf.py --version # ESP-IDF v5.3.2-dirty
|
||||||
|
```
|
||||||
|
2. **Werror=format-truncation** — `snprintf` with 320B buffers flagged. Add `-Wno-format-truncation` via `target_compile_options` (safe, buffers intentionally oversized).
|
||||||
|
3. **Direct font symbols** — Any `&lv_font_montserrat_*` will compile but fail at runtime. Always use `lvgl_get_text_font()`.
|
||||||
|
4. **Asset size bloat** — Bundling 4MB monolithic bin triggers OOM at runtime. Split per-book and stream.
|
||||||
|
4b. **Book browser OOM — 66 buttons = 275B heap free** (July 11 BibleVerse on 192.168.68.112 0.7.0-dev). Building 66× `lv_button_create` + labels + nested flex rows = 132-264 LVGL objs → heap `total 310KB free 275B min_free 23 largest_block 23` → `failed to create temp directory` on install, screenshot 500. Fix: v4 clean horizontal slide ~10 objs (no top bar/Go/card, center LARGE 50% bigger directly on bg tappable, bottom row [X left][slider flex][39/66 right]), swipe via `LV_EVENT_PRESSED/PRESSING` tracking `book_drag_start_x` 28px threshold, plus `lv_slider` fast scrub. After fix heap `10943B free largest 7168` on .112, `54471→54355` stable on .129 0.8.0-dev (0.8 has more heap: 64KB pre). ELF itself lives in PSRAM (`CONFIG_ELF_LOADER_LOAD_PSRAM=y`, 5MB free), but LVGL objects are internal heap only — PSRAM doesn't help browser. Editorial v4 also adds fade transition `lv_anim_*` 220/300ms no leak.
|
||||||
|
5. **Touch input swallowed by full-screen containers — GameKit bubbling pitfall** (PocketDungeon July 16 v3) — Symptom: after adding `introContainer` + `gameContainer` full-screen covering `root`, `GameKit::attachInput(root, scene)` alone stopped working. Tap/swipe dead. Root cause: LVGL event bubbling — topmost `LV_OBJ_FLAG_CLICKABLE` container captures `CLICKED`/`GESTURE` before `root`. Also `rulesBox` inside introContainer swallowed taps needed for `pointToQuadrant`. Fix pattern `attachInputToCurrent()`:
|
||||||
|
- Re-attach input to root + current active containers with `CLICKABLE|GESTURE_BUBBLE`
|
||||||
|
- Make inner info boxes non-clickable/non-scrollable so taps bubble to parent
|
||||||
|
- Start button calls `lv_event_stop_bubbling(e)` to prevent double trigger
|
||||||
|
- Handle `Touch`/`Swipe` InputKind at game-over restart
|
||||||
|
- After fix + rebuild `esp32s3`, touch quadrants + swipe + keys work on 192.168.68.112. See `references/intro-fullscreen.md` v3 section.
|
||||||
|
6. **Fullscreen toolbar-less apps trap user — no exit** (PocketDungeon July 16 v4) — Removing `tt_lvgl_toolbar_create_for_app` gains 40-50px but loses system back button → no way to exit. Fix: three exit paths:
|
||||||
|
- (1) Hardware: `InputKind::Cancel/Menu` (Q/ESC → Cancel via `keyToInput` maps q/Q → Cancel, `LV_KEY_ESC` → Cancel) → `tt_app_stop()`, include via `TactilityCpp/App.h` (pulls `tt_app.h`), save bests + SFX Cancel
|
||||||
|
- (2) X button top-right in game phase (`LV_SYMBOL_CLOSE` 36×28 bg #2A2D40 radius 6 → `onExitButtonClicked`)
|
||||||
|
- (3) Long-press board `LV_EVENT_LONG_PRESSED` on `gameContainer` → pause overlay `AppPhase::Paused` with Resume/Exit Game (overlay bg #000 OPA_70, box 180px, Resume blue accent #365CF5, Exit grey #3A3A4A, hint Q/ESC = exit / Long press = pause); any input in Paused resumes. Intro also has Exit button. Header adds `pauseOverlay` + `Paused` + 3 static callbacks. Verification: hermes-verify-exit*.py checks `tt_app_stop`, `pauseOverlay`, `LONG_PRESSED`, `Cancel`, build ok.
|
||||||
|
7. **Black bar crop + Georgia sizing + 15px lift correction** (BibleVerse v6 July 12) — Verse cropped by header bar `size 100%x44 bg #15151F COVER + border 1 #232338` overlapping center col. User: "black bar on top cropping text" + "reduce text size for this font" + "main text can be moved a bit up 30px" then corrected "should have been 15px". Fix: header `36px` (was 44), `bg 0x0E0E14 TRANSP 0` (was 15151F COVER), border 0, pad ver 4, center `PCT(90)x82% align CENTER 0,8` initially → `-22` (30px up) → corrected to `-7` (15px up: base +8 -15). Ctrl bar also TRANSP, verse pad 4. Georgia Italic wider than Montserrat → downtier: `LARGE→22` (was 26), `DEFAULT→18` (was 20), `SMALL→16`, ultra `<40c` still 26 via `resolve_verse_font_ultra`. Pads tighter `6/4/3/2/1` (was 10/8/6/4/2). Vision: Hab 2:9 "No extra black bar overlapping, fully visible" + Zeph 1:3 "balanced now, optical center correct" PASS, heap 46215-47591 stable. Screenshot rate-limit 2s+ to avoid `ConnectionResetError 54` heap spike. Gotcha: `lv_timer_t` incomplete in SDK — `t->user_data` invalid, must use `lv_timer_get_user_data(t)` (exported). See `references/black-bar-crop.md` + `references/pretty-font.md`.
|
||||||
|
8. **Book browser animation + auto-show 10s** (BibleVerse v6 July 12) — User: "add animation to the bible book selection. Also let's start the app with the menu showing, but make the menu UI auto hide after 10s." Impl: `AppCtx { lv_timer_t* chrome_auto_hide_timer; bool ui_visible; }`, `chrome_auto_hide_cb(lv_timer_t* t){ AppCtx* ctx = lv_timer_get_user_data(t); if(book_browser_visible) return; set_chrome_visible(false); delete timer; }`, `schedule_chrome_auto_hide(ctx,10000)` creates timer, `onShowApp` `set_chrome_visible(true)` + schedule 10s, `toggle_ui` re-arms 10s when shown else deletes, `toggle_book_browser` deletes auto-hide timer, `onHideApp` cleanup. Book browser anim: `book_y_anim_cb(void* var,int32 v){ lv_obj_set_y((lv_obj_t*)var,v); }` + `book_fade_anim_cb` → `text_opa`, `set_book_browser_visible(true)` → `remove FLAG_HIDDEN`, `mid_wrap y 20→0 260ms ease_out` + name fade `OPA_20→COVER 220ms`, `book_dial_update_center` also fades name `60→255 180ms` on swipe/slider change. Verified .129: 8683 bytes at 0.8s after run menu visible → 6882 hidden at ~2s, auto-hide 10s configured. See `references/immersive-ui.md` + `references/book-browser.md`.
|
||||||
|
9. **Assuming `lv_font_montserrat_18` exists** — Not even compiled for all boards. Stick to SMALL/DEFAULT/LARGE.
|
||||||
|
10. **Build cache stale after main.c edit** — `tactility.py clean` before rebuild when CMake glob changes.
|
||||||
|
11. **Install without platform arg** — `tactility.py Apps/X install <ip>` tries all `manifest platforms`; if only esp32s3 ELF built you get `ELF file not built for esp32p4`. Pass `esp32s3` explicitly.
|
||||||
|
12. **6666 refused** — Dev server disabled. Use dashboard `PUT /api/apps/install` on port 80, or enable Developer mode on device Settings first.
|
||||||
|
13. **Wrong board flashed (color vs RLCD)** — Family has two boards: ES3C28P (2.8" color, `es3c28p`) and waveshare-esp32-s3-rlcd (4.2" mono). Symptoms: monochrome UI = flashed RLCD firmware onto color board. Always `grep CONFIG_TT_DEVICE_ID sdkconfig` and `ls /dev/cu.usbmodem*` before flash. Family color board is ES3C28P at `/dev/cu.usbmodem101`, 16MB flash@80MHz, `partitions-16mb-with-sd.csv`.
|
||||||
|
14. **Personal remote app not yet pulled locally** — User says pull it from repo (2026-07-16). PocketDungeon existed only on personal remote (git.reynafamily.com/adolforeyna/tactility_apps) commit d976595 feat: add Pocket Dungeon game prototype, not in local Apps/ after fetch of origin. Fix `cd apps && git fetch personal && git merge personal/main`.
|
||||||
|
15. **Board ending in 112 shorthand** — User shorthand ending on 112 means WiFi IP in local subnet. Discovery: 192.168.68.0/24 scan, 192.168.68.112 ping OK (ESP32-S3, osVersion 0.8.0-dev, MAC 14:c1:9f:d1:56:90), 192.168.68.111 also online = 2 boards active. No USB present is fine for network install via port 6666 or dashboard 80. Use arp + socket probe. Must pass esp32s3 platform arg else fails with ELF file not built for esp32.
|
||||||
|
16. **GameKit / SfxEngine shared libraries (2026-07-16)** — New pattern for game apps. `main/CMakeLists.txt` globs `../../Libraries/GameKit/Source/*.c*` + `SfxEngine` + INCLUDE_DIRS + REQUIRES `TactilitySDK esp_driver_i2s esp_driver_gpio`. GameKit provides Loop, Scene, Input (arrows/WASD+gestures+quadrant), Grid, Draw, Prefs. App entry still registerApp. 40KB .app tar contains elf/esp32s3.elf. Extended v2-v4 with AppPhase Intro/Playing/Paused, introContainer+gameContainer+pauseOverlay, `attachInputToCurrent()` bubbling fix, exit handling via Cancel/Menu + X + long-press. See `references/pocket-dungeon.md` + `references/intro-fullscreen.md` v3/v4.
|
||||||
|
|
||||||
|
## Verification Checklist
|
||||||
|
|
||||||
|
- [ ] `TACTILITY_SDK_PATH` set, IDF `export.sh` sourced, using IDF python binary for `tactility.py`
|
||||||
|
- [ ] `build/<App>.app` exists, `tar tvf` shows expected assets
|
||||||
|
- [ ] `xtensa-esp32s3-elf-nm -D *.app.elf` undefined checked; 0 missing vs firmware exports (0 undef total v4 editorial = healthy)
|
||||||
|
- [ ] No direct `lv_font_montserrat_*` or `LV_SYMBOL_STAR` refs in source (grep `lv_font_montserrat`)
|
||||||
|
- [ ] `main/CMakeLists.txt` has `-Wno-format-truncation` allowance
|
||||||
|
- [ ] Settings path via `tt_app_get_user_data_child_path`, tested load/save
|
||||||
|
- [ ] If immersive UI: tap toggle, timer 60s, hidden chrome by default verified on device/sim, fade 220/300ms no leak heap stable >10KB
|
||||||
|
- [ ] If book browser: ~10 objs, [X left][slider flex][N/66 right], no top bar/Go/card, LARGE name 50% bigger tappable, swipe 28px, heap >10KB (275B = OOM, 31KB+error dialog = missing symbol)
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- `references/build-env.md` — full env error transcript + fix
|
||||||
|
- `references/symbols.md` — exported vs missing symbol list, OOM vs missing-symbol diagnosis, heap sizes `275B vs 31KB vs 54KB stable`, anim/binfont exports
|
||||||
|
- `references/offline-data.md` — per-book binary format and converter usage
|
||||||
|
- `references/deploy.md` — dashboard vs dev-port install, PUT /api/apps/install, platform arg handling, discovery via browser tools, .112 0.7.0-dev vs .129 0.8.0-dev fleet
|
||||||
|
- `references/pocket-dungeon.md` — 2026-07-16 PocketDungeon GameKit+SfxEngine via GLOB_RECURSE, personal remote pull (git.reynafamily.com), board shorthand 112 discovery, PYTHONPATH fix, 40KB .app build + network install esp32s3
|
||||||
|
- `references/intro-fullscreen.md` — Intro state machine AppPhase + fullscreen no-toolbar, rules/controls screen, HideStatusBar flag not parsed for ELF apps
|
||||||
|
- `references/sdk-mismatch-missing-symbol.md` — SDK/OS version mismatch .112 0.8.0-dev vs 0.7 manifest + unexported `lv_obj_set_style_max_width` / `scrollbar_mode` causing `missing symbol` dialog, QVGA cramped fix, `/api/screenshot` remote QA
|
||||||
|
- `references/immersive-ui.md` — UI pattern v4 editorial: warm #F2F0E8, uppercase tracking 2 ref #9A9AA8 gap 10, fade transition anim, binfont future
|
||||||
|
- `references/book-browser.md` — book selector evolution: 66-button OOM → arc rotary (0.7 missing) → horizontal slide v3 → clean v4 no top/Go/card LARGE name + [X][slider][N/66], PSRAM vs heap
|
||||||
|
- `references/black-bar-crop.md` — v6 black bar 44px #15151F COVER overlapping verse crop fix, Georgia down-tier LARGE→22 DEFAULT→18 memo, screenshot rate-limit
|
||||||
|
- `references/audio-apps.md` — RLCD Mp3Player task stack/prio/YIELD + VoiceRecorder stereo→mono downmix + ES7210 vs ES8311 MCLK/DMA fixes (2026-07-12)
|
||||||
|
- `scripts/verify_symbols.py` — rerunnable undefined-symbol checker
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# RLCD Audio Apps — Mp3Player & VoiceRecorder Fixes (2026-07-12)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Waveshare RLCD 4.2" uses ES8311 DAC @0x18 + ES7210 mic ADC @0x40, MCLK=12.288MHz GPIO16, BCLK=9 WS=45 TX=8 RX=10 AMP=46 HIGH.
|
||||||
|
Working reference: `MicroPython/test1/Screen/lib/audio_util.py` + `board_config.py` WAVESHARE_RLCD profile.
|
||||||
|
|
||||||
|
## Mp3Player Pitfalls
|
||||||
|
|
||||||
|
### 1. Task Stack & Priority
|
||||||
|
- Default `xTaskCreate(..., 4096, 5)` → stack overflow under minimp3 + SD + I2S + LVGL.
|
||||||
|
- Log: `i2s_alloc_dma_desc failed` sometimes masked as OOM, but also task crashes.
|
||||||
|
- Fix: `6144, prio 6`.
|
||||||
|
|
||||||
|
### 2. taskYIELD Causing Repetition
|
||||||
|
```c
|
||||||
|
// Before: decode one MP3 frame (1152 samples @16k = 72ms) then:
|
||||||
|
taskYIELD(); // gives CPU to LVGL but DMA empties → repeats last buffer
|
||||||
|
// After: remove YIELD, loop tight, let I2S write block (250ms timeout) drive pacing
|
||||||
|
```
|
||||||
|
- DMA 4x120 = 30ms total, smaller than frame time → underrun → repetition. Increased driver DMA to 6x160 (~60ms) + removed YIELD.
|
||||||
|
|
||||||
|
### 3. Sample Rate Assumptions
|
||||||
|
- RLCD files in repo are 16k mono (faith-mysterious-garden/page001.mp3 31652 bytes, 16k 1ch).
|
||||||
|
- `I2S configured: 16000 Hz, 1 channels` — must match ES8311 boot init that already programs 16k exact.
|
||||||
|
- Reclock hook that rewrites ES8311 0x06/0x07/0x08 during I2S start causes pop. For 16k, skip entirely.
|
||||||
|
- MCLK multiple: 16k*768=12.288MHz exact, 32k*384, 48k*256. Default SDK 256 gives 4.096MHz for 16k → robot sound. See `tactility-firmware/references/rlcd-audio-fix.md`.
|
||||||
|
|
||||||
|
## VoiceRecorder Pitfalls
|
||||||
|
|
||||||
|
### Mono vs Stereo for ES7210
|
||||||
|
Working MP:
|
||||||
|
```python
|
||||||
|
i2s = I2S(1, sck=9, ws=45, sd=10, mode=I2S.RX, rate=16000, bits=16, format=I2S.STEREO)
|
||||||
|
# then mono extract: for i in 0..n step 4: mono[j]=buf[i:i+2]
|
||||||
|
```
|
||||||
|
- ES7210 outputs stereo (2 mics) even if you want mono file.
|
||||||
|
- VoiceRecorder previously requested `channel_right = NONE` (mono slot mode) → driver expects 1 slot but codec sends 2 → low pitch mic-like.
|
||||||
|
- Fix: Request `channel_right=1` (STEREO), read `AUDIO_BUF_SIZE` (4096) stereo, downmix left channel in-place backward to avoid overwrite:
|
||||||
|
```c
|
||||||
|
size_t mono_bytes = bytes_read/2;
|
||||||
|
for(int src=bytes_read-4, dst=mono_bytes-2; src>=0; src-=4, dst-=2){
|
||||||
|
buf[dst]=buf[src]; buf[dst+1]=buf[src+1];
|
||||||
|
}
|
||||||
|
fwrite(buf,1,mono_bytes,f);
|
||||||
|
```
|
||||||
|
- WAV header stays mono (1 ch, byterate 32000) – data correctly pitched.
|
||||||
|
|
||||||
|
### TDM vs STD
|
||||||
|
- Other boards (m5stack-stackchan, tab5) use TDM 4-slot for ES7210 via `i2s_controller_set_rx_tdm_config`.
|
||||||
|
- RLCD working MP uses STD stereo, not TDM. Keep STD for RLCD. TDM path DMA also needs reduction (was 8x512 → 4x120) or OOM.
|
||||||
|
|
||||||
|
## Firmware / Logging Interaction
|
||||||
|
|
||||||
|
### File Lock Flood
|
||||||
|
`File.cpp:getLock()` logged `WARN File lock function not set!` on every `listDirectory()` – Tactility scans `/sdcard` every sec → 1Hz UART spam.
|
||||||
|
- UART ISR at 115200 contends with I2S GDMA ISR → jitter → small noises.
|
||||||
|
- Fix: once-only warning + listDir INFO→DEBUG.
|
||||||
|
|
||||||
|
### I2S Pin Logs
|
||||||
|
`esp32_i2s: Configuring I2S pins...` logged INFO on every play → same jitter.
|
||||||
|
- Fix: LOG_D.
|
||||||
|
|
||||||
|
### Verification Checklist
|
||||||
|
- Play 16k mono MP3: no pop at start, no robot, no repetition.
|
||||||
|
- Log after fix: no `File lock flood`, no `i2s_alloc_dma_desc failed`.
|
||||||
|
- Record WAV, download, play on PC: 16k mono correct pitch, not low/mic-like.
|
||||||
|
- Serial for voice: `I2S configured: 16000 Hz, 1 channels` for play, stereo read for record is internal.
|
||||||
|
|
||||||
|
## References
|
||||||
|
- `tactility-firmware/references/rlcd-audio-fix.md` – GPIO5 conflict, correct pins, DMA OOM, MCLK, ES8311 reclock
|
||||||
|
- MicroPython working code: `~/Projects/MicroPython/test1/Screen/lib/board_config.py` WAVESHARE_RLCD
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# v6 Black Bar Crop Fix + 15px Lift Correction — July 12 BibleVerse on .129 final 2142871
|
||||||
|
|
||||||
|
**Symptom:**
|
||||||
|
Vision: "black bar on the top cropping the text" — Habakkuk screenshots 4539 bytes showed 44px dark bar `#15151F COVER` + border overlapping center column, cutting first line. Also "reduce text size a bit for this font" — Georgia Italic 26pt wider than Montserrat LARGE. Then "main text can be moved a bit up 30px, seems padding favoring empty space on top." → corrected "We sent the text 30px up, but I think it should had been 15".
|
||||||
|
|
||||||
|
**Root cause header:**
|
||||||
|
```c
|
||||||
|
lv_obj_set_size(hb, LV_PCT(100), 44); bg #15151F COVER border 1 #232338
|
||||||
|
center_col lv_obj_center size 90%x80%
|
||||||
|
verse pad_all 6 font 26
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix v6 final 15px lift:**
|
||||||
|
1. Header transparent same root no border smaller:
|
||||||
|
```c
|
||||||
|
size 36 not 44; bg #0E0E14 same as parent root was #15151F; bg_opa TRANSP was COVER; border 0 was 1; pad_ver 4 was 0
|
||||||
|
```
|
||||||
|
2. Ctrl bar also TRANSP editorial: bg #0E0E14 TRANSP border 0 was #15151F COVER + top border
|
||||||
|
3. Center shift + lift correction:
|
||||||
|
```c
|
||||||
|
// was center
|
||||||
|
size 90%x82% // 80->82
|
||||||
|
align CENTER 0,8 // +8 down to clear statusbar
|
||||||
|
// then 30px up request -> 0,-22
|
||||||
|
// then correction 15px up -> 0,-7 = +8 base -15 = 15px up balanced final
|
||||||
|
// pad verse 4 not 6
|
||||||
|
```
|
||||||
|
Vision Zeph 1:3 after -7: "balanced now, optical center correct, prevents sitting on buttons, 1.5x padding above controls, no excessive top space" PASS. No black bar Hab 2:9 "No extra black bar overlapping, fully visible".
|
||||||
|
|
||||||
|
4. Georgia Italic down-tier wider than Montserrat:
|
||||||
|
```c
|
||||||
|
Before: LARGE->26 DEFAULT->20 SMALL->16
|
||||||
|
After v6: LARGE->22 was 26, DEFAULT->18 was 20, SMALL->16, ultra <40c still 26 via resolve_verse_font_ultra
|
||||||
|
pads tighter: <40 6/20 not 10/28, <100 4/16 not 8/24, <200 3/14 not 6/20, <350 2/12 not 4/16, 350+ 1/8 not 2/12
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Menu auto-show 10s + book anim: See immersive-ui.md and book-browser.md v6 final. Timer gotcha: `t->user_data` incomplete typedef error in SDK 0.8.0-dev → must use `lv_timer_get_user_data(t)` (exported 382/388). Verified .129 menu show 8683 bytes.
|
||||||
|
|
||||||
|
**Verification final 2142871:**
|
||||||
|
- Build 4.3M `tactility.py build esp32s3 --local-sdk` ok, ELF 0 missing
|
||||||
|
- Install .129: heap `46215-47591 free largest 20480 psram 4810396` stable, ss `5335 serif` vs `3991 sans` vs `8683 menu visible` vs `6882 immersive`
|
||||||
|
- Vision checks: Hab 2:9 no crop, Zeph 1:3 balanced 15px lift, menu chrome visible at 0.8s then hidden showing premium cinematic
|
||||||
|
- Screenshot rate-limit >2s between to avoid `ConnectionResetError 54` heap spike from encoder
|
||||||
|
|
||||||
|
**Lessons:**
|
||||||
|
- Avoid opaque header on immersive — use root bg TRANSP so no crop when mistakenly visible
|
||||||
|
- When swapping Mont→Georgia reduce one tier — 26pt serif ~22pt sans visual
|
||||||
|
- Lift correction: base CENTER 0,8 too low empty top → 30px up → user corrected 15px up → -7 final, always confirm lift target in vision
|
||||||
|
- Timer incomplete typedef: use getter, not direct field
|
||||||
|
- Screenshot burst OOM — wait 2s+
|
||||||
|
|
||||||
|
See immersive-ui.md v6 final + pretty-font.md v6 final + book-browser.md v6 final. Beautiful per user final approval.
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# Book Browser — Horizontal Slide + Clean v4 + Animated v6 Final (arc-free, low-RAM, beautiful)
|
||||||
|
|
||||||
|
## Problem history
|
||||||
|
|
||||||
|
1. **66-button list** — 66× `lv_button_create` + flex rows + labels = 132-264 LVGL objs.
|
||||||
|
- `GET /api/sysinfo`: `heap free 275B total 310KB min_free 23 largest_block 23` → OOM.
|
||||||
|
- Symptom: `PUT /api/apps/install 500 failed to create temp directory`, screenshot `500 25`.
|
||||||
|
- After self-crash: heap 33KB, install `200 ok` again.
|
||||||
|
|
||||||
|
2. **`lv_arc` rotary** — center name changes rotating ring, 6 objs ideal.
|
||||||
|
- Build 0 missing on 0.8.0-dev SDK (`symbols.c` PR #496 `dff93cb6`), but device `0.7.0-dev` (192.168.68.112) missing: blue dialog `Error / missing symbol / OK`, heap `31779` healthy → loader failure not OOM.
|
||||||
|
- `git log -S lv_arc_create` → added after 0.7 release. Must be arc-free for 0.7 fleet.
|
||||||
|
- `lv_roller` not exported on 0.7/0.8.
|
||||||
|
|
||||||
|
3. **v3 horizontal slide + slider** — works on 0.7, ~12 objs, heap `10943 free largest 7168` (112), `54355 free` on 129 0.8.0-dev after editorial.
|
||||||
|
|
||||||
|
## v4 Clean (July 12) — User: remove Go, card, top bar; larger 50% name; number 39/66 right of slider; cancel left of slide
|
||||||
|
|
||||||
|
- Removed: top header `Books — swipe`, Go button 200x36, card bg `15151F` radius 18
|
||||||
|
- New layout:
|
||||||
|
- `center_wrap` full-screen `PCT(100)` pad_top 0 pad_bottom 64 (was 52/88), transparent, CLICKABLE + swipe `PRESSED/PRESSING/RELEASED/PRESS_LOST`
|
||||||
|
- Left arrow 44x44 radius22 `1E1E2E` at `LEFT_MID 8,0`, right arrow same `RIGHT_MID -8,0`, click steps ±1
|
||||||
|
- Center `mid_wrap` 68% width `SIZE_CONTENT`, transparent, gap 10, flex column center, CLICKABLE → `on_book_center_click` jumps (tap name does Go)
|
||||||
|
- `book_center_name` `LARGE` (was DEFAULT, 50% bigger), width `100%`, `DOTS`, center aligned, pretty `georgia_regular_24` serif
|
||||||
|
- `book_center_verses` small dim `5A5A78`
|
||||||
|
- Bottom bar `100%x56` `15151F` top border `232338`, row `SPACE_BETWEEN` center, gap 10, pad hor10 ver8
|
||||||
|
- `[X cancel 36x32 radius8 252538 left] [slider flex_grow 1 height8 range 0..65 bg 232338 ind 4A4A6E knob E8E6E0] [num label 1/66 right 8A8AAA small]`
|
||||||
|
- Object count ~10 (vs 12 v3, vs 264 v1) → heap `54471→54355` stable on 129, `PSRAM 4.9MB`
|
||||||
|
|
||||||
|
## v6 Final Animated (July 12 beautiful build 2142871) — User: "add animation to the bible book selection. Also let's start the app with the menu showing, but make the menu UI auto hide after 10s." + lift correction 30→15px + beautiful approval
|
||||||
|
|
||||||
|
**Animation added**:
|
||||||
|
```c
|
||||||
|
static void book_y_anim_cb(void* var,int32_t v){ lv_obj_set_y((lv_obj_t*)var,v); }
|
||||||
|
static void book_fade_anim_cb(void* var,int32_t v){ lv_obj_set_style_text_opa((lv_obj_t*)var,v,0); }
|
||||||
|
|
||||||
|
set_book_browser_visible(true):
|
||||||
|
ctx->book_dial_idx = cur_book_idx; book_dial_update_center(ctx);
|
||||||
|
remove FLAG_HIDDEN
|
||||||
|
// pop center mid_wrap y 20->0 260ms ease_out
|
||||||
|
mid_wrap y = 20; lv_anim 20->0 260ms ease_out via book_y_anim_cb
|
||||||
|
// fade name OPA_20->COVER 220ms ease_out via book_fade_anim_cb
|
||||||
|
book_dial_update_center: on swipe/slider change name fade 60->255 180ms ease_out
|
||||||
|
```
|
||||||
|
|
||||||
|
**Menu auto-show 10s** (see immersive-ui.md): `chrome_auto_hide_timer` field + `chrome_auto_hide_cb` uses `lv_timer_get_user_data` not `t->user_data` (incomplete typedef error), `onShowApp` `set_chrome_visible(true)` + `schedule 10000`, `toggle_ui` re-arms 10s, `toggle_book_browser` deletes timer, `onHide` deletes. Verified 8683 bytes menu visible `Zephaniah 1:3 / 31094` + bottom pills X/List/Prev/Pause/Next at 0.8s after run, 6882 immersive hidden at ~2s (configured 10s, screenshot race). "This is beatiful, well done."
|
||||||
|
|
||||||
|
**Lift correction**: `CENTER 0,8` too low → `0,-22` 30px up → user corrected "should had been 15" → `0,-7` = `+8 base -15` = 15px up balanced, vision "optical center correct, prevents sitting on buttons".
|
||||||
|
|
||||||
|
## AppCtx fields v6 final
|
||||||
|
```c
|
||||||
|
lv_obj_t* book_browser;
|
||||||
|
lv_obj_t* book_center_name; // LARGE 50% bigger, directly on bg, tappable jumps, georgia_regular_24
|
||||||
|
lv_obj_t* book_center_num; // bottom right 39/66
|
||||||
|
lv_obj_t* book_center_verses;
|
||||||
|
lv_obj_t* book_slider;
|
||||||
|
int book_dial_idx;
|
||||||
|
int book_drag_start_x;
|
||||||
|
bool book_dragging;
|
||||||
|
bool book_browser_visible;
|
||||||
|
lv_timer_t* chrome_auto_hide_timer; // v6 auto-hide
|
||||||
|
bool ui_visible;
|
||||||
|
```
|
||||||
|
|
||||||
|
Center update syncs slider + number + fade:
|
||||||
|
```c
|
||||||
|
static void book_dial_update_center(AppCtx* ctx){
|
||||||
|
BookInfo* b=&ctx->books[ctx->book_dial_idx];
|
||||||
|
// fade anim 60->255 180ms
|
||||||
|
lv_obj_set_style_text_opa(center_name,100,0);
|
||||||
|
lv_anim_t an; lv_anim_init(&an); lv_anim_set_var(&an,center_name); lv_anim_set_values(&an,60,255); lv_anim_set_duration(&an,180);
|
||||||
|
lv_anim_set_exec_cb(&an,book_fade_anim_cb); lv_anim_set_path_cb(&an,lv_anim_path_ease_out); lv_anim_start(&an);
|
||||||
|
lv_label_set_text(book_center_name,b->bname);
|
||||||
|
lv_obj_set_style_text_font(book_center_name,resolve_book_font(ctx),0); // georgia_regular_24 50% bigger
|
||||||
|
snprintf(num,"%2d / %d",b->bnumber,book_count); lv_label_set_text(book_center_num,num);
|
||||||
|
snprintf(vers,"%d verses",b->verse_count); lv_label_set_text(book_center_verses,vers);
|
||||||
|
lv_slider_set_value(slider,dial,LV_ANIM_OFF);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Swipe 28px per book via `lv_indev_get_point/active`.
|
||||||
|
|
||||||
|
## PSRAM vs heap
|
||||||
|
- `CONFIG_ELF_LOADER_LOAD_PSRAM=y` → app `.text/.rodata` SPIRAM 4.2-4.3MB ELF, free 4.8-5.2MB
|
||||||
|
- LVGL objects internal heap 310KB only. 264 objs OOM, 10 objs safe, 13 objs with anim safe 46-47KB
|
||||||
|
|
||||||
|
## Compat table v6 final
|
||||||
|
| widget | 0.7.0-dev .112 | 0.8.0-dev .129 | notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `lv_arc_*` | MISSING | exported PR #496 | true rotary if fleet 0.8+ |
|
||||||
|
| `lv_roller_*` | MISSING | MISSING | not exported |
|
||||||
|
| `lv_slider_*` | OK | OK | scrub |
|
||||||
|
| `lv_indev_get_point/active` | OK | OK | swipe 28px |
|
||||||
|
| `lv_anim_*` | OK | OK | fade 220/300, book slide 260ms |
|
||||||
|
| `lv_timer_get_user_data` | OK | OK | auto-hide timer, need getter not direct access |
|
||||||
|
| `georgia_italic_*` embedded | OK via fallback | OK 4.3M PSRAM | pretty serif |
|
||||||
|
|
||||||
|
## Verification v6 final beautiful
|
||||||
|
- Heap 275B OOM → 10943 .112 → 54471→46215 .129 stable with fonts+anim
|
||||||
|
- Screenshots: reading 3756-4347, editorial fade 3991, menu show 8683, immersive 6882, error dialog 2755 missing symbol
|
||||||
|
- Boards .112 0.7.0 33KB, .129 0.8.0 46-54KB
|
||||||
|
- Build 2142871 editorial v6 beautiful approved
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# Build Env — Transcript & Fix
|
||||||
|
|
||||||
|
## Error 1: Hermes venv pollution — TypeError on `X | Y`
|
||||||
|
|
||||||
|
```
|
||||||
|
File .../hermes-agent/venv/lib/python3.11/site-packages/requests/__init__.py
|
||||||
|
import urllib3
|
||||||
|
File .../urllib3/_base_connection.py
|
||||||
|
bytes, typing.IO[typing.Any], typing.Iterable[bytes | str], str
|
||||||
|
TypeError: unsupported operand type(s) for |: 'type' and 'type'
|
||||||
|
```
|
||||||
|
IDF Python is 3.9.6 (no `X | Y` unions). Hermes venv injects `PYTHONPATH` with Python 3.11's `urllib3` that uses 3.10+ syntax. Affects `tactility.py` which imports `requests`.
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
```bash
|
||||||
|
. /Users/adolforeyna/esp/esp-idf/export.sh
|
||||||
|
export TACTILITY_SDK_PATH=/Users/adolforeyna/Projects/Tactility/firmware/release/TactilitySDK
|
||||||
|
export PYTHONPATH="" # critical
|
||||||
|
/Users/adolforeyna/.espressif/python_env/idf5.3_py3.9_env/bin/python tactility.py Apps/BibleVerse build esp32s3 --local-sdk
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error 2: pydantic_core shadowing — idf.py itself crashes
|
||||||
|
|
||||||
|
```
|
||||||
|
Cannot import module "pydantic_core._pydantic_core".
|
||||||
|
Please use idf.py only in an ESP-IDF shell environment.
|
||||||
|
ModuleNotFoundError: No module named 'pydantic_core._pydantic_core'
|
||||||
|
```
|
||||||
|
Hermes venv 3.11's `pydantic_core` has `_pydantic_core.cpython-311-darwin.so` built for 3.11. IDF venv 3.9/3.10 has `.cpython-39/310.so`. Because `PYTHONPATH` includes Hermes site-packages, Python tries to load 3.11 `.so` under 3.9/3.10 → fails.
|
||||||
|
|
||||||
|
Same error manifests as:
|
||||||
|
```
|
||||||
|
'/.../idf5.3_py3.9_env/bin/python' is currently active ... project was configured with '/.../idf5.3_py3.10_env/bin/python'. Run 'idf.py fullclean'
|
||||||
|
```
|
||||||
|
Solution: clear env, set IDF_PYTHON_ENV_PATH, fullclean.
|
||||||
|
|
||||||
|
### Full Fix (this session — ES3C28P firmware build)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/adolforeyna/Projects/Tactility/firmware
|
||||||
|
unset PYTHONPATH; unset PYTHONHOME
|
||||||
|
export IDF_PYTHON_ENV_PATH=/Users/adolforeyna/.espressif/python_env/idf5.3_py3.9_env
|
||||||
|
. /Users/adolforeyna/esp/esp-idf/export.sh
|
||||||
|
|
||||||
|
# Verify fix:
|
||||||
|
$IDF_PYTHON_ENV_PATH/bin/python -c "import pydantic_core; print('ok')"
|
||||||
|
idf.py --version # ESP-IDF v5.3.2-dirty
|
||||||
|
|
||||||
|
python device.py es3c28p # regenerates sdkconfig
|
||||||
|
idf.py fullclean # when switching python version
|
||||||
|
idf.py build # -> build/Tactility.bin
|
||||||
|
idf.py -p /dev/cu.usbmodem101 flash # flash family board
|
||||||
|
```
|
||||||
|
|
||||||
|
Build output for ES3C28P (16MB, 80MHz, partitions-16mb-with-sd.csv):
|
||||||
|
```
|
||||||
|
Wrote 2863136 bytes (1653045 compressed) at 0x00010000 in 19.9s (1149.4 kbit/s)
|
||||||
|
bootloader 0x0, partition-table 0x8000, Tactility.bin 0x10000, system.bin 0x410000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error 3: IDF_PATH unset in subshell
|
||||||
|
`zsh -c 'python3 tactility.py...'` loses `IDF_PATH`. Always source inside same shell or use wrapper script.
|
||||||
|
|
||||||
|
## Error 4: format-truncation as error
|
||||||
|
IDF adds `-Werror=all`. `snprintf(dst, 320, "%s/bible", assets_path)` flagged even though 320B is generous.
|
||||||
|
Fix: `target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-format-truncation -Wno-unused-but-set-variable)`
|
||||||
|
|
||||||
|
## Correct build commands
|
||||||
|
|
||||||
|
Firmware core:
|
||||||
|
```
|
||||||
|
cd /Users/adolforeyna/Projects/Tactility/firmware
|
||||||
|
unset PYTHONPATH; unset PYTHONHOME
|
||||||
|
export IDF_PYTHON_ENV_PATH=/Users/adolforeyna/.espressif/python_env/idf5.3_py3.9_env
|
||||||
|
. /Users/adolforeyna/esp/esp-idf/export.sh
|
||||||
|
python device.py es3c28p
|
||||||
|
idf.py build
|
||||||
|
idf.py -p /dev/cu.usbmodem101 flash
|
||||||
|
```
|
||||||
|
|
||||||
|
ELF app (BibleVerse):
|
||||||
|
```
|
||||||
|
cd /Users/adolforeyna/Projects/Tactility/apps
|
||||||
|
. ~/esp/esp-idf/export.sh > /dev/null
|
||||||
|
env -u PYTHONPATH /Users/adolforeyna/.espressif/python_env/idf5.3_py3.9_env/bin/python tactility.py Apps/BibleVerse build esp32s3 --local-sdk
|
||||||
|
# => build/BibleVerse.app 4.2MB, ESP32S3 ELF 17K
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Files
|
||||||
|
- `/Users/adolforeyna/Projects/Tactility/board_compilation_learnings.md` — full v5.3.2 compatibility fixes (I2C backport, missing cstring, etc.)
|
||||||
|
- This session also fixed wrong board: `waveshare-esp32-s3-rlcd` flashed onto color `es3c28p`. Always `grep CONFIG_TT_DEVICE_ID sdkconfig`.
|
||||||
|
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# Deploy & Run on Device (v0.7+ Dashboard)
|
||||||
|
|
||||||
|
## Board Discovery (.112 case)
|
||||||
|
|
||||||
|
User said "ip ending on 112". Local subnet found via `ifconfig` -> `192.168.68.114/24`, so target is `192.168.68.112`.
|
||||||
|
|
||||||
|
Discovery recipe:
|
||||||
|
```bash
|
||||||
|
arp -a | grep 112
|
||||||
|
# ping sweep
|
||||||
|
for cand in 192.168.68.112 192.168.1.112 192.168.0.112; do
|
||||||
|
ping -c1 -W 500 $cand
|
||||||
|
curl -m2 -s http://$cand/ | head
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Found: port 80 responds with HTTP 302 -> `/dashboard.html`. Port 6666 (old dev port) Connection refused.
|
||||||
|
|
||||||
|
## Install Endpoint Change
|
||||||
|
|
||||||
|
`tactility.py` (tool v3.5.1) still tries `POST http://<ip>:6666/app/install`. That requires Developer mode enabled on device (Settings > Development > Enable dev server). When disabled, install fails with:
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ Install request failed: HTTPConnectionPool(host='192.168.68.112', port=6666): ... Connection refused
|
||||||
|
```
|
||||||
|
|
||||||
|
Firmware v0.7.0-dev Dashboard is on port 80 with new API:
|
||||||
|
|
||||||
|
- Discovered via browser devtools / `document.documentElement.innerHTML` JS fetch search:
|
||||||
|
- `GET /api/apps` -> list
|
||||||
|
- `PUT /api/apps/install` multipart field `file`
|
||||||
|
- `POST /api/apps/run?id=<appId>`
|
||||||
|
- `POST /api/apps/uninstall?id=<appId>`
|
||||||
|
- `/api/sysinfo`, `/api/screenshot`, etc.
|
||||||
|
|
||||||
|
### Working install via curl / python
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://192.168.68.112/api/apps | jq
|
||||||
|
|
||||||
|
# PUT, not POST
|
||||||
|
curl -X PUT http://192.168.68.112/api/apps/install \
|
||||||
|
-F "file=@build/BibleVerse.app;type=application/octet-stream"
|
||||||
|
# => 200 ok
|
||||||
|
```
|
||||||
|
|
||||||
|
Python:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests, pathlib
|
||||||
|
ip="192.168.68.112"
|
||||||
|
path="build/BibleVerse.app"
|
||||||
|
with open(path,'rb') as f:
|
||||||
|
r=requests.put(f"http://{ip}/api/apps/install",
|
||||||
|
files={'file': ('BibleVerse.app', f, 'application/octet-stream')},
|
||||||
|
timeout=120)
|
||||||
|
print(r.status_code, r.text[:500])
|
||||||
|
# run
|
||||||
|
requests.post(f"http://{ip}/api/apps/run?id=one.tactility.bibleverse", timeout=10)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Platform Arg Required
|
||||||
|
|
||||||
|
`tactility.py Apps/MyApp install <ip>` without platform tries all `manifest platforms` (esp32s3,esp32p4) -> fails if only esp32s3 ELF built:
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ ELF file not built for esp32p4
|
||||||
|
```
|
||||||
|
|
||||||
|
Fix: always pass platform explicitly matching what you built:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tactility.py Apps/BibleVerse install 192.168.68.112 esp32s3
|
||||||
|
# or
|
||||||
|
tactility.py Apps/BibleVerse build esp32s3,esp32p4 --local-sdk # build all first
|
||||||
|
```
|
||||||
|
|
||||||
|
## Browser Verification Recipe
|
||||||
|
|
||||||
|
Use Hermes browser tools:
|
||||||
|
```js
|
||||||
|
// navigate to http://<ip>/dashboard.html -> Apps tab
|
||||||
|
document.documentElement.innerHTML.match(/\/api\/apps[^\s"']*/g)
|
||||||
|
// returns ["/api/apps", "/api/apps/run?id=", "/api/apps/uninstall?id=", "/api/apps/install"]
|
||||||
|
// inline scripts contain:
|
||||||
|
function installAppFile(file) { fetch('/api/apps/install', {method:'PUT', body: formData}) }
|
||||||
|
```
|
||||||
|
|
||||||
|
This avoids guessing old docs.
|
||||||
|
|
||||||
|
## Checklist After Install
|
||||||
|
|
||||||
|
- [ ] GET /api/apps contains your app id
|
||||||
|
- [ ] POST /api/apps/run?id= -> 200 ok (app shows on device)
|
||||||
|
- [ ] If install via dashboard UI: drag .app onto upload zone works same as PUT
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# Immersive Single-Verse UI — BibleVerse pattern (v6 final 15px lift + menu auto-show)
|
||||||
|
|
||||||
|
From BookPlayer + BibleVerse: fullscreen verse, chrome now auto-shows 10s on start then hides, tap toggles, adaptive scaling + editorial + fade + embedded Georgia Italic serif + clean book browser + animations.
|
||||||
|
|
||||||
|
## v6 Final July 12 — Beautiful build 2142871
|
||||||
|
|
||||||
|
User: "We sent the text 30px up, but I think it should had been 15" → corrected -22 → -7 (15px up). Also "It seems as if there is a black bar on the top cropping the text. Also it seems we need to reduce the text size a bit for this font." + "First, let's save the learnings about UI and Fonts on the app skill, and then let's also add animation to the bible book selection. Also let's start the app with the menu showing, but make the menu UI auto hide after 10s." + "This is beatiful, well done."
|
||||||
|
|
||||||
|
**Black bar crop**: Header `44px bg #15151F COVER border 1 #232338` overlapping verse. Fix `36px bg 0x0E0E14 TRANSP 0` same as root, border 0, pad ver 4. Ctrl bar TRANSP. Vision Hab 2:9 "No extra black bar overlapping, fully visible" PASS.
|
||||||
|
|
||||||
|
**Georgia sizing down**: Georgia Italic wider than Montserrat → LARGE→22 was 26, DEFAULT→18 was 20, SMALL→16, ultra <40c still 26 via `resolve_verse_font_ultra`. Pads tighter `6/4/3/2/1` was 10/8/6/4/2. Heap 47591 stable.
|
||||||
|
|
||||||
|
**Lift correction 30px→15px**: Was `CENTER 0,8` too low (empty top). 30px attempt `0,-22`. User corrected to 15px → `0,-7` = `+8 base -15`. Vision Zeph 1:3 "balanced now, optical center correct, prevents sitting on buttons, 1.5x padding above controls" PASS. 8683 bytes menu visible, 6882 immersive.
|
||||||
|
|
||||||
|
**Menu auto-show 10s**: Previously hidden by default. Now:
|
||||||
|
```c
|
||||||
|
typedef struct { lv_timer_t* chrome_auto_hide_timer; bool ui_visible; } AppCtx;
|
||||||
|
static void chrome_auto_hide_cb(lv_timer_t* t){
|
||||||
|
AppCtx* ctx = (AppCtx*)lv_timer_get_user_data(t); // NOT t->user_data incomplete -> invalid use error, must use getter (exported 382/388)
|
||||||
|
if(ctx->book_browser_visible) return;
|
||||||
|
if(ctx->ui_visible) set_chrome_visible(ctx,false);
|
||||||
|
lv_timer_delete(t); ctx->chrome_auto_hide_timer=NULL;
|
||||||
|
}
|
||||||
|
static void schedule_chrome_auto_hide(AppCtx* ctx,uint32_t ms){
|
||||||
|
if(ctx->chrome_auto_hide_timer){ lv_timer_delete(ctx->chrome_auto_hide_timer); ctx->chrome_auto_hide_timer=NULL; }
|
||||||
|
ctx->chrome_auto_hide_timer = lv_timer_create(chrome_auto_hide_cb, ms, ctx);
|
||||||
|
}
|
||||||
|
onShowApp: set_chrome_visible(&g_ctx,true); schedule_chrome_auto_hide(&g_ctx,10000);
|
||||||
|
toggle_ui: if new_vis schedule 10s else delete; toggle_book_browser deletes timer; onHide deletes.
|
||||||
|
```
|
||||||
|
Verified .129 192.168.68.129: 0.8s after run 8683 bytes menu visible top `Zephaniah 1:3 / 31094` + bottom pills X/List/Prev/Pause/Next "premium cinematic Audible-like" -> ~2s 6882 hidden (timer + screenshot heap race, configured 10000). Heap 46215-47399 stable PSRAM 4.8MB.
|
||||||
|
|
||||||
|
**Book browser animation**:
|
||||||
|
```c
|
||||||
|
static void book_y_anim_cb(void* var,int32_t v){ lv_obj_set_y((lv_obj_t*)var,v); }
|
||||||
|
static void book_fade_anim_cb(void* var,int32_t v){ lv_obj_set_style_text_opa((lv_obj_t*)var,v,0); }
|
||||||
|
set_book_browser_visible(true): remove FLAG_HIDDEN; mid_wrap y 20->0 260ms ease_out + name fade OPA_20->COVER 220ms
|
||||||
|
book_dial_update_center: name fade 60->255 180ms on swipe/slider change
|
||||||
|
```
|
||||||
|
Swipe 28px per book via `lv_indev_get_point/active`, slider fast scrub, bottom row [X left][slider flex][39/66 right], no card/Go/top bar, LARGE pretty name 50% bigger tappable `georgia_regular_24`.
|
||||||
|
|
||||||
|
**Timer gotcha**: `lv_timer_t user_data` incomplete in SDK 0.8.0-dev -> `t->user_data` = `invalid use of incomplete typedef`. Must use `lv_timer_get_user_data(t)` (DEFINE_MODULE_SYMBOL 382/388). Grep `grep timer symbols.c`.
|
||||||
|
|
||||||
|
**Screenshot rate-limit**: Rapid double `GET /api/screenshot` causes `ConnectionResetError 54` + heap spike crash (encoder uses internal heap). Wait 2s+ between shots, check `GET /api/sysinfo` heap recovery to ~47KB before next.
|
||||||
|
|
||||||
|
## Scaling tiers v6 final (Georgia resolved)
|
||||||
|
| len | Fallback | Georgia | line_sp | pad | Example |
|
||||||
|
|-----|----------|---------|---------|-----|---------|
|
||||||
|
| <40 | LARGE | 26 ultra | 12 | 6/20 | John 11:35 11c |
|
||||||
|
| 40-99 | LARGE | 22 | 8 | 4/16 | Gen 2:10 77c |
|
||||||
|
| 100-199 | DEFAULT | 18 | 6 | 3/14 | typical |
|
||||||
|
| 200-349 | SMALL | 16 | 5 | 2/12 | long |
|
||||||
|
| 350+ | SMALL | 16 | 4 | 1/8 | Esther 8:9 |
|
||||||
|
Center `90%x82% align 0,-7` = 15px up balanced.
|
||||||
|
|
||||||
|
## v5 Pretty Serif — Georgia Italic like PSALMS 23:1 card
|
||||||
|
Generation: `npx -y lv_font_conv --font "/System/Library/Fonts/Supplemental/Georgia Italic.ttf" -r 32-126 -r 0x2018-0x201D --size 26 --bpp 4 --format lvgl --lv-include "lvgl.h" --no-compress -o Source/fonts/georgia_italic_26.c` repeat 22/20/18/16 + regular 24. Sizes 26=85KB 22=65KB 20=57KB 18=50KB 16=43KB regular24=68KB ~368KB src ~315KB .rodata PSRAM via `CONFIG_ELF_LOADER_LOAD_PSRAM=y`. `LV_FONT_DECLARE` + resolve helpers fallback to `lvgl_get_text_font()`. Heap 47539 stable vs 47855 pre, 5336 bytes serif vs 3991 sans.
|
||||||
|
|
||||||
|
## v4 Editorial — PSALMS 23:1 white reference
|
||||||
|
Verse warmer `#F2F0E8` flat radius 0 pad 4, ref uppercase `EPHESIANS 1:4` tracking 2 `#9A9AA8` gap 10 was 14, fade 220ms verse /300ms ref via `lv_anim_*` exported since 0.6 safe, heap 54KB stable.
|
||||||
|
|
||||||
|
## Checklist v6 final
|
||||||
|
- [ ] header TRANSP bg 0E0E14 36px border 0, ctrl TRANSP, center -7 15px up balanced PASS Zeph 1:3
|
||||||
|
- [ ] serif downtier LARGE->22 DEFAULT->18 SMALL->16 ultra 26 pads 6/4/3/2/1
|
||||||
|
- [ ] menu shows 8683 on start then auto-hide 10s via lv_timer_get_user_data, tap re-arms, book browser deletes timer
|
||||||
|
- [ ] book browser slide up 20->0 260ms + fade 20->COVER 220ms + dial fade 60->255 180ms
|
||||||
|
- [ ] heap >40KB, PSRAM 4.8MB, 0 missing syms, beautiful per user commit 2142871
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
# Intro screen + fullscreen pattern for Tactility games (2026-07-16 v2-v4)
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
User wants:
|
||||||
|
1. Game engine assessment
|
||||||
|
2. Fullscreen games
|
||||||
|
3. Intro screen with rules/movements before game
|
||||||
|
4. No way to exit (fullscreen lost toolbar back button)
|
||||||
|
5. Touch unresponsive after fullscreen refactor
|
||||||
|
|
||||||
|
## Engine assessment given
|
||||||
|
GameKit Loop/Scene/Input/Grid/Draw/Prefs is enough for MVP:
|
||||||
|
- Input unification (LV_KEY_* + WASD + gesture + touch quadrant via pointToQuadrant) covers all 40+ boards
|
||||||
|
- Grid helpers keep 9x7 logic clean
|
||||||
|
- Model/Renderer split solid, Prefs + SfxEngine integration painless
|
||||||
|
- Gaps: no scene-stack, no tween/flash, proc-gen fixed 2 walls + 2 enemies, 8 entity cap, no FOV, no inventory
|
||||||
|
|
||||||
|
## Fullscreen reality
|
||||||
|
- Firmware has `AppManifest::Flags::HideStatusBar` used by Boot, Setup, TouchCalibration (GuiService.cpp hide/show via `flags.hideStatusbar`)
|
||||||
|
- **BUT** ELF external manifest parsing `AppManifestParsingV1.cpp / V2.cpp` only parses:
|
||||||
|
- id, name, versionName, versionCode, target.sdk, target.platforms
|
||||||
|
- **Ignores flags** — so external apps cannot hide statusbar without firmware patch
|
||||||
|
- What you CAN do today: don't call `tt_lvgl_toolbar_create_for_app(parent, app)` → gain ~40-50px vertical
|
||||||
|
- Breakout, Snake keep toolbar for scores/lives
|
||||||
|
- For PocketDungeon we dropped it: root bg set to COLOR_BG, container 100% x 100% no border, board centered with dynamic cell calc `(screenW-22)/COLS` and `(screenH-70)/ROWS`
|
||||||
|
- If true edge-to-edge needed: patch `AppManifestParsingV1/V2` to parse `app.flags=HideStatusBar` or `manifest.properties` line `flags=...`, rebuild firmware via `python device.py es3c28p && idf.py build flash -p /dev/cu.usbmodem101`, rebuild SDK, rebuild app
|
||||||
|
|
||||||
|
## Intro screen implementation (state machine AppPhase) — v2
|
||||||
|
Pattern added to PocketDungeon.cpp/h:
|
||||||
|
|
||||||
|
### Header
|
||||||
|
```cpp
|
||||||
|
enum class AppPhase : uint8_t { Intro, Playing, Paused };
|
||||||
|
class PocketDungeon final : public App, public GameKit::Scene {
|
||||||
|
lv_obj_t* introContainer = nullptr;
|
||||||
|
lv_obj_t* gameContainer = nullptr;
|
||||||
|
lv_obj_t* pauseOverlay = nullptr;
|
||||||
|
AppPhase phase = AppPhase::Intro;
|
||||||
|
void showIntro();
|
||||||
|
void showGame();
|
||||||
|
void showPause(); void hidePause();
|
||||||
|
void clearIntro(); void clearGame();
|
||||||
|
void renderIntro();
|
||||||
|
void attachInputToCurrent();
|
||||||
|
void exitGame();
|
||||||
|
static void onStartButtonClicked(lv_event_t* e);
|
||||||
|
static void onResumeButtonClicked(lv_event_t* e);
|
||||||
|
static void onExitButtonClicked(lv_event_t* e);
|
||||||
|
static void onGameContainerLongPress(lv_event_t* e);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flow v2
|
||||||
|
- `onShow`: set bg, prefs load, model.reset(lv_tick_get()), Sfx start, phase=Intro, onEnter, loop.start(200ms, this) — NO toolbar creation for fullscreen-ish
|
||||||
|
- `onEnter`: GameKit::attachInput(root,this), showIntro()
|
||||||
|
- `showIntro()`: clearGame+clearIntro, phase=Intro, renderIntro()
|
||||||
|
- renderIntro creates full-screen col flex container bg=COLOR_BG, title "POCKET DUNGEON", subtitle, divider line 120x2 accent, rulesBox 90% width flex_grow 1 bg=COLOR_PANEL radius 10:
|
||||||
|
- GOAL (gold #D99B23): "Reach deepest floor..."
|
||||||
|
- HOW TO PLAY (green #3B9D58): "@ = You s = Slime b = Bat $ = Treasure (+5) > = Stairs # = Wall Bump to attack"
|
||||||
|
- CONTROLS (blue #365CF5): "Arrows / WASD / Swipe / Quadrant tap, Q/ESC = exit, Long press = pause"
|
||||||
|
- Start button 160x44 bg accent radius10 -> showGame(), Exit button 160x36 bg #2A2D40, footer Best
|
||||||
|
- `showGame()`: clearIntro+clearGame, phase=Playing, create gameContainer 100% TRANSP no scroll, CLICKABLE|GESTURE_BUBBLE, renderer.create, add X button top-right 36x28 bg #2A2D40 SYMBOL_CLOSE -> exitGame, attachInputToCurrent, render, sfx Warp
|
||||||
|
- `onInput`: Cancel/Menu -> exitGame (Q/ESC). Intro: any Unknown != -> showGame. Paused: any -> hidePause. Playing: dx/dy, gameOver reset, else movePlayer
|
||||||
|
- `render()`: if Intro|Paused return, else renderer.render
|
||||||
|
|
||||||
|
## v3 Touch unresponsive fix — THE CRITICAL PITFALL
|
||||||
|
**Symptom**: After adding introContainer + gameContainer full-screen covering root, touch/swipe dead, only hardware keys work.
|
||||||
|
|
||||||
|
**Root cause**: LVGL event bubbling. `GameKit::attachInput(target, scene)` does:
|
||||||
|
```cpp
|
||||||
|
lv_obj_add_flag(target, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
lv_obj_add_event_cb(target, inputEvent, LV_EVENT_KEY, scene);
|
||||||
|
lv_obj_add_event_cb(target, inputEvent, LV_EVENT_GESTURE, scene);
|
||||||
|
lv_obj_add_event_cb(target, inputEvent, LV_EVENT_CLICKED, scene);
|
||||||
|
```
|
||||||
|
If you only attach to `root`, but `introContainer` (100% child) is CLICKABLE and covers root, it receives CLICKED/GESTURE first and doesn't forward to root. Also `rulesBox` inside introContainer swallowed quadrant taps.
|
||||||
|
|
||||||
|
**Fix pattern `attachInputToCurrent()`**:
|
||||||
|
```cpp
|
||||||
|
void attachInputToCurrent() {
|
||||||
|
if (root) GameKit::attachInput(root, this);
|
||||||
|
if (introContainer) {
|
||||||
|
lv_obj_add_flag(introContainer, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
lv_obj_add_flag(introContainer, LV_OBJ_FLAG_GESTURE_BUBBLE);
|
||||||
|
GameKit::attachInput(introContainer, this);
|
||||||
|
}
|
||||||
|
if (gameContainer) {
|
||||||
|
lv_obj_add_flag(gameContainer, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
lv_obj_add_flag(gameContainer, LV_OBJ_FLAG_GESTURE_BUBBLE);
|
||||||
|
GameKit::attachInput(gameContainer, this);
|
||||||
|
lv_obj_add_event_cb(gameContainer, onGameContainerLongPress, LV_EVENT_LONG_PRESSED, this);
|
||||||
|
}
|
||||||
|
if (pauseOverlay) {
|
||||||
|
lv_obj_add_flag(pauseOverlay, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
GameKit::attachInput(pauseOverlay, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Call on every phase change.
|
||||||
|
- `rulesBox` must be `remove_flag SCROLLABLE` and NOT CLICKABLE — let taps bubble to introContainer for `pointToQuadrant`.
|
||||||
|
- Start button calls `lv_event_stop_bubbling(e)` to prevent double trigger (button click + quadrant).
|
||||||
|
- Accept `Touch`/`Swipe` InputKind at gameOver restart.
|
||||||
|
|
||||||
|
## v4 Exit handling for toolbar-less fullscreen
|
||||||
|
**Problem**: No toolbar = no system back button = trapped.
|
||||||
|
|
||||||
|
**Three exit paths implemented**:
|
||||||
|
1. Hardware: `InputKind::Cancel` (Q/q key via `keyToInput` maps q/Q -> Cancel, LV_KEY_ESC -> Cancel) or `Menu` -> `exitGame()` -> `tt_app_stop()` (from `tt_app.h`, already included via `TactilityCpp/App.h`). Call `saveBests()` + Sfx `Cancel`.
|
||||||
|
2. X button: small 36x28 top-right in game phase, bg #2A2D40 radius 6, `LV_SYMBOL_CLOSE`, event `onExitButtonClicked` -> `tt_app_stop()`.
|
||||||
|
3. Long-press: `LV_EVENT_LONG_PRESSED` on `gameContainer` -> `showPause()` creates overlay 100% bg #000 OPA_70 flex center, box 180px bg PANEL radius 12, Resume (accent) + Exit Game (grey) + hint. `AppPhase::Paused` — any input resumes, buttons stop bubbling.
|
||||||
|
|
||||||
|
Intro also has Exit button. All paths save bests.
|
||||||
|
|
||||||
|
### Header additions v4
|
||||||
|
```cpp
|
||||||
|
lv_obj_t* pauseOverlay = nullptr;
|
||||||
|
enum AppPhase { Intro, Playing, Paused };
|
||||||
|
void showPause(); void hidePause(); void exitGame();
|
||||||
|
static void onResumeButtonClicked(lv_event_t* e);
|
||||||
|
static void onExitButtonClicked(lv_event_t* e);
|
||||||
|
static void onGameContainerLongPress(lv_event_t* e);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verification (ad-hoc tmp script hermes-verify-*.py)
|
||||||
|
Checks: no toolbar_create_for_app, has AppPhase Intro/Playing/Paused, showIntro/showGame/showPause, ENTER DUNGEON, GOAL+CONTROLS, CLICKABLE+GESTURE_BUBBLE, LONG_PRESSED, tt_app_stop, Cancel/Menu exit, build produces package esp32s3.
|
||||||
|
Build: `PYTHONPATH='' python3 tactility.py Apps/PocketDungeon build esp32s3 --local-sdk` -> package OK
|
||||||
|
Install: `... install 192.168.68.112 esp32s3 --local-sdk` + `run`
|
||||||
|
|
||||||
|
### v5-v6 QVGA left-info + right-small-icons redesign
|
||||||
|
|
||||||
|
User feedback July 16: "Still not great layout. I think we can replace text button with icons small on the right, and have the information on the side left."
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
- Header: title POCKET DUNGEON + subtitle + Best F3 G18 compact (pad 6, row 2)
|
||||||
|
- mainRow: flex ROW, flex_grow 1, pad 4/8 column, bottom 8, no scroll, border 0 TRANSP
|
||||||
|
- leftCol: flex_grow 1, height 100%, pad_right 4 bottom 6, column row 8, SCROLLABLE (safety for QVGA overflow)
|
||||||
|
- makeCard(PCT 100%, SIZE_CONTENT, pad 9/3, bg PANEL radius10): GOAL (gold) + HOW TO PLAY (green) only = 2 cards for airy fit; HOW TO PLAY merges symbols + Controls hint "Arrows/WASD/Swipe/Quadrant"
|
||||||
|
- rightRail: fixed 44px wide, SIZE_CONTENT height, column center, row 10, SCROLLABLE removed
|
||||||
|
- makeIconBtn lambda (symbol, bg, cb, user, size): btn size s×s, bg, radius s/3, pad 0, event CLICKED
|
||||||
|
- Play 38px accent #365CF5 SYMBOL_PLAY, Exit 30px muted #252836 SYMBOL_CLOSE
|
||||||
|
- Result: info left 75%, icons small right 25%, no bottom clipping (2 cards vs 3 saves ~60px), screenshot intro_v6.png 6.4K vision: "Structure correct – 2 cards left, play/exit right – but scrollbar visible indicating near overflow; still dense but no clip" → final fix adds pad_bottom to mainRow + leftCol and reduces rail to 44px CONTENT vs 100% height to avoid LVGL flex 100% child overflow bug.
|
||||||
|
|
||||||
|
Pitfalls fixed:
|
||||||
|
- LVGL flex row with child height 100% inside flex_grow parent can overflow on 320x198 usable (QVGA after 20px statusbar). Use rightRail SIZE_CONTENT, not 100%, and leftCol SCROLLABLE with pad_bottom.
|
||||||
|
- Avoid `set_style_max_width` / `set_scrollbar_mode` (unexported) → use PCT widths 92/80 for gutters.
|
||||||
|
- Keep card count ≤2 for QVGA intro to stay airy; third card pushes to scroll clip.
|
||||||
|
|
||||||
|
### Future ideas
|
||||||
|
- Game-over -> intro loop instead of immediate restart
|
||||||
|
- Cell flash animation on Attacked/Blocked using lv_anim (exported since 0.6, safe)
|
||||||
|
- Push to personal/main after user likes exit flow
|
||||||
|
- True statusbar hide via firmware patch parsing flags
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Per-Book Offline Bible Assets
|
||||||
|
|
||||||
|
Source: https://github.com/sajeevavahini/bibles — Zefania XML `World English Bible.xml` ~4.8MB, format `XMLBIBLE/BIBLEBOOK/CHAPTER/VERS` attrs `bnumber/bname/cnumber/vnumber`.
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
- 4.8MB XML → 3.8MB blob + 304KB idx monolith = too big for RAM, packaging bloats `.app` to 4.2MB monolith load.
|
||||||
|
- Device has ~8MB PSRAM but LVGL/lwIP compete; loading whole bible at once OOM-prone.
|
||||||
|
|
||||||
|
## Solution: per-book split
|
||||||
|
|
||||||
|
Output `assets/bible/`:
|
||||||
|
- `books.json`: `[{bnumber, bname, verses, chapters}, ...]` 66 entries
|
||||||
|
- `<bnum>.bin` (01.bin..66.bin): concatenated NUL-terminated UTF-8 verse strings
|
||||||
|
- `<bnum>.idx`: binary: header 8 bytes: magic `BIBK` ascii, version u16 (1), count u32; then per entry 8 bytes: `offset u32 LE, cnum u16 LE, vnum u16 LE` sorted by chapter/verse.
|
||||||
|
|
||||||
|
Worst book: Psalms 19.bin ~220KB, still fits. Total ~4.1MB split but loaded one at a time → RAM tiny.
|
||||||
|
|
||||||
|
## Converter
|
||||||
|
|
||||||
|
`Apps/BibleVerse/tools/convert_bible.py` (also template):
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
```bash
|
||||||
|
python3 tools/convert_bible.py /path/to/World\ English\ Bible.xml assets/bible/
|
||||||
|
# or fetch then convert
|
||||||
|
```
|
||||||
|
|
||||||
|
Implementation sketch (see templates/convert_bible.py):
|
||||||
|
- xml.etree.ElementTree iterative parse `BIBLEBOOK` to keep memory low.
|
||||||
|
- Accumulate bin buffer + idx entries per book.
|
||||||
|
- Write books.json last.
|
||||||
|
|
||||||
|
## Hybrid resolution on device
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 1. Try embedded A: assets via tt_app_get_assets_path
|
||||||
|
// 2. Fallback scan /sdcard/bibles/WEB/ same binary format for user-provided translations
|
||||||
|
// 3. build target: load BookInfo array, then on demand load_book(bnumber) frees previous.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verify packaging
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tar tvf build/BibleVerse.app | grep "\.bin" | wc -l # expect 66
|
||||||
|
ls -lh assets/bible/*.bin | sort -k5 -hr | head
|
||||||
|
```
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# PocketDungeon — GameKit + SfxEngine + tutorial levels (2026-07-16/17)
|
||||||
|
|
||||||
|
User said "board ending on 112" → WiFi discovery to 192.168.68.112. Repo pull needed from personal remote. Infinite floors, tutorial-by-play intro.
|
||||||
|
|
||||||
|
## Repo layout & build
|
||||||
|
- `Apps/PocketDungeon/CMakeLists.txt`: standard `tactility_project`
|
||||||
|
- `main/CMakeLists.txt`: GLOB_RECURSE GameKit + SfxEngine via `../../../Libraries/...` + `REQUIRES TactilitySDK esp_driver_i2s esp_driver_gpio`
|
||||||
|
- `manifest.properties`: sdk=0.8.0-dev (bumped from 0.7 to match board OS 0.8.0-dev, else missing symbol dialog), platforms esp32,esp32s3,esp32c6,esp32p4
|
||||||
|
- GameKit: Loop, Scene (Tick, InputKind), Input keyToInput/gestureToInput/pointToQuadrant/attachInput, Grid, Draw, Prefs
|
||||||
|
- Personal remote `git.reynafamily.com/adolforeyna/tactility_apps` commit d976595 feat: add Pocket Dungeon prototype, pull via `git fetch personal && git merge personal/main`
|
||||||
|
- Build env pitfall: Hermes PYTHONPATH pollution → `PYTHONPATH='' python3 tactility.py Apps/PocketDungeon build esp32s3 --local-sdk` + `IDF_PYTHON_ENV_PATH=idf5.3_py3.9_env`
|
||||||
|
- Board shorthand .112: ping 192.168.68.112 OK (ESP32-S3 MAC 14:c1:9f:d1:56:90, os 0.8.0-dev), .111 also online. Install needs platform arg `esp32s3` else fails ELF not built for esp32. Dashboard port 80 `PUT /api/apps/install`, dev 6666.
|
||||||
|
|
||||||
|
## v2-v4: Intro + fullscreen + input + exit
|
||||||
|
- Engine enough for MVP: Loop/Scene/Input/Grid, but no scene-stack/tween/proc-gen depth.
|
||||||
|
- Fullscreen: firmware HideStatusBar flag ignored by ELF parser V1/V2 → cannot hide statusbar without firmware patch. Gain ~40-50px by not calling `tt_lvgl_toolbar_create_for_app`.
|
||||||
|
- Intro state machine AppPhase Intro/Playing/Paused, introContainer+gameContainer+pauseOverlay, attachInputToCurrent() re-attaches CLICKABLE|GESTURE_BUBBLE to root+active container, rulesBox non-clickable so quadrant taps bubble, start button stop_bubbling.
|
||||||
|
- Touch dead after fullscreen → fixed via attachInputToCurrent() pattern.
|
||||||
|
- Exit trap fullscreen → Q/ESC Cancel/Menu → tt_app_stop() (via TactilityCpp/App.h), X top-right 32x26 SYMBOL_CLOSE, long-press LONG_PRESSED → pause overlay (bg #000 OPA_70, box 180px Resume accent #365CF5 grey Exit #252836).
|
||||||
|
- See references/intro-fullscreen.md v3/v4.
|
||||||
|
|
||||||
|
## v5-v6: QVGA left info + right small icons (cramped fix)
|
||||||
|
User Jul 16: "Still not great layout. I think we can replace text button with icons small on the right, and have the information on the side left."
|
||||||
|
- mainRow flex ROW flex_grow 1 pad 4/8 column bottom 8, leftCol flex_grow 1 scrollable (pad_row 8 bottom 6), rightRail fixed 44px SIZE_CONTENT column center row 10.
|
||||||
|
- Left cards initially GOAL + HOW TO PLAY (2 cards), right icons Play 38 accent + Exit 30 muted (SYMBOL_PLAY/CLOSE radius s/3). Verified via /api/screenshot 6-7KB PNG, no max_width/scrollbar_mode (unexported → missing symbol blue OK dialog). SDK bump 0.7→0.8 fixed.
|
||||||
|
- Symbol hygiene: no `lv_obj_set_style_max_width`, no `set_scrollbar_mode`. Use PCT widths.
|
||||||
|
|
||||||
|
## v7: Tutorial levels instead of text explanations (Jul 17)
|
||||||
|
User: "Let's add three intro levels instead of explanations. First level present the player and the stairs. Next level the bat and the gold. Both with two rows and regular cols. Then go the current first level. So on the initial page, only keep the goal and the controls explanation."
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
- Model.h: `TUTORIAL_ROWS=2`, `renderRows` in DungeonState, `floor 0,1 = tutorial`, `displayFloor()` maps tutorial→1 real floor-1, `isTutorial()`, `tutorialId()`, `reset(seed, tutorials=true)` floor 0 if tutorials else 2, `generateTutorialFloor(tId)`, `clearAllTiles()`.
|
||||||
|
- Model.cpp:
|
||||||
|
- clearAllTiles border walls
|
||||||
|
- Tut0: renderRows=TUTORIAL_ROWS, hide rows >=3 walls, player {1,1}, stairs COLS-2,1 only
|
||||||
|
- Tut1: + treasure {3,1} + bat {5,1} hp1 + stairs, no random walls
|
||||||
|
- Real >=2: previous logic treasure [5][2+floor%2], walls, enemies scale realFloor=floor-1, best update only floor>=2, moveMonsters skipped when isTutorial
|
||||||
|
- Renderer.cpp: respects renderRows hides cells HIDDEN, board height = cellPx*TUTORIAL_ROWS centered -10 for tutorial, status "TUTORIAL X/2 HP Gold" message "TUT 1: Move to > @=you" / "TUT 2: Kill b, take $, use >"
|
||||||
|
- PocketDungeon.cpp: intro only GOAL + CONTROLS (2 cards), no HOW TO PLAY – symbols taught via play. Right rail 44px Play 38 Exit 30 stays.
|
||||||
|
|
||||||
|
Flow: Intro (Goal+Controls left, icons right) → Tut1 2×9 @→> movement → Tut2 2×9 @+b+$→> combat/loot → Real F1 infinite (displayFloor). Infinite floors – answer to user "how many floors" = infinite.
|
||||||
|
|
||||||
|
Verification: hermes-verify-tutorial*.py checks 2 cards, no HOW TO PLAY, tutorial gen, renderRows, TUTORIAL_ROWS, isTutorial, build package, nm -D 0 missing (89 undef). Installed 192.168.68.112 intro_tutorial.png 6.3K.
|
||||||
|
|
||||||
|
## Floors
|
||||||
|
Infinite unbounded, difficulty scales slime hp floor/3, extra enemy >=3. No win boss. User asked, answered infinite.
|
||||||
|
|
||||||
|
## Next todos
|
||||||
|
- Push to personal/main (requested earlier)
|
||||||
|
- True HideStatusBar via firmware patch AppManifestParsingV1/V2 flags
|
||||||
|
- Victory floor 10 boss + win screen
|
||||||
|
- Cell flash anim Attacked/Blocked via lv_anim (exported since 0.6)
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# Pretty Serif Font — Georgia Italic embedded (BibleVerse v5/v6 final July 12 - beautiful build 2142871)
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
User: "The main text has a pretty basic font" + white PSALMS 23:1 reference serif italic high-contrast. Montserrat sans functional not editorial.
|
||||||
|
|
||||||
|
Goal: serif italic verse, elegant book dial, heap stable 47KB, 0 missing syms, PSRAM-aware, beautiful per user "This is beatiful, well done."
|
||||||
|
|
||||||
|
## Options
|
||||||
|
1. `lv_binfont_create(path)` → `.fnt` bin via VFS A:/assets — exported 0.8 but path fragile, logs not avail, needs `tt_app_get_assets_path` void check `buf[0]!='\0'`.
|
||||||
|
2. **Embedded C LVGL fonts (chosen)** → `lv_font_conv --format lvgl` → `LV_FONT_DECLARE` → linked direct. Lives ELF `.rodata` → `MALLOC_CAP_SPIRAM` via `CONFIG_ELF_LOADER_LOAD_PSRAM=y` → PSRAM 4.8MB free, not internal 310KB heap. No VFS, no runtime open, 0 missing syms.
|
||||||
|
|
||||||
|
## Generation v6 final
|
||||||
|
```bash
|
||||||
|
FONT="/System/Library/Fonts/Supplemental/Georgia Italic.ttf"
|
||||||
|
RFONT="/System/Library/Fonts/Supplemental/Georgia.ttf"
|
||||||
|
OUT=Apps/BibleVerse/main/Source/fonts
|
||||||
|
mkdir -p $OUT
|
||||||
|
for SZ in 26 22 20 18 16; do npx -y lv_font_conv --font "$FONT" -r 32-126 -r 0x2018-0x201D --size $SZ --bpp 4 --format lvgl --lv-include "lvgl.h" --no-compress -o $OUT/georgia_italic_${SZ}.c; done
|
||||||
|
npx -y lv_font_conv --font "$RFONT" -r 32-126 --size 24 --bpp 4 --format lvgl --lv-include "lvgl.h" --no-compress -o $OUT/georgia_regular_24.c
|
||||||
|
# ls -lh $OUT: 26=85KB 22=65KB 20=57KB 18=50KB 16=43KB regular24=68KB ~368KB src ~315KB binary .rodata
|
||||||
|
```
|
||||||
|
Fix: `--lv-include "lvgl.h"` else generates `#include "lvgl/lvgl.h"` fail in SDK (header is `lvgl.h`).
|
||||||
|
|
||||||
|
CMake: `file(GLOB_RECURSE SOURCE_FILES Source/*.c)` auto-includes `Source/fonts/*.c`.
|
||||||
|
|
||||||
|
## Code wiring v6 final (15px lift)
|
||||||
|
```c
|
||||||
|
LV_FONT_DECLARE(georgia_italic_26)
|
||||||
|
LV_FONT_DECLARE(georgia_italic_22)
|
||||||
|
LV_FONT_DECLARE(georgia_italic_20)
|
||||||
|
LV_FONT_DECLARE(georgia_italic_18)
|
||||||
|
LV_FONT_DECLARE(georgia_italic_16)
|
||||||
|
LV_FONT_DECLARE(georgia_regular_24)
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
lv_font_t* font_serif_26; lv_font_t* font_serif_22; lv_font_t* font_serif_20;
|
||||||
|
lv_font_t* font_serif_18; lv_font_t* font_serif_16; lv_font_t* font_book_24;
|
||||||
|
bool fonts_loaded;
|
||||||
|
// + chrome_auto_hide_timer etc
|
||||||
|
} AppCtx;
|
||||||
|
|
||||||
|
// v6 downtier: Georgia wider/taller than Montserrat → map down one to avoid crop
|
||||||
|
static const lv_font_t* resolve_verse_font(AppCtx* ctx, enum LvglFontSize f){
|
||||||
|
if(!ctx->fonts_loaded) return lvgl_get_text_font(f);
|
||||||
|
if(f==FONT_SIZE_LARGE && ctx->font_serif_22) return ctx->font_serif_22; // was 26 too big -> 22
|
||||||
|
if(f==FONT_SIZE_DEFAULT && ctx->font_serif_18) return ctx->font_serif_18; // was 20 -> 18
|
||||||
|
if(f==FONT_SIZE_SMALL && ctx->font_serif_16) return ctx->font_serif_16;
|
||||||
|
return lvgl_get_text_font(f);
|
||||||
|
}
|
||||||
|
static const lv_font_t* resolve_verse_font_ultra(AppCtx* ctx){
|
||||||
|
return ctx->font_serif_26 ? (lv_font_t*)&georgia_italic_26 : lvgl_get_text_font(FONT_SIZE_LARGE);
|
||||||
|
}
|
||||||
|
static const lv_font_t* resolve_book_font(AppCtx* ctx){
|
||||||
|
if(ctx->font_book_24) return ctx->font_book_24;
|
||||||
|
if(ctx->font_serif_22) return ctx->font_serif_22;
|
||||||
|
return lvgl_get_text_font(FONT_SIZE_LARGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
onShowApp:
|
||||||
|
g_ctx.font_serif_26 = (lv_font_t*)&georgia_italic_26; ...
|
||||||
|
g_ctx.font_book_24 = (lv_font_t*)&georgia_regular_24;
|
||||||
|
g_ctx.fonts_loaded = true;
|
||||||
|
|
||||||
|
onHide: static const - no free (live in PSRAM ELF)
|
||||||
|
|
||||||
|
apply_verse_scaling: pads tighter v6 6/4/3/2/1 was 10/8/6/4/2 + resolve_verse_font + lift -7 (15px up not 30px)
|
||||||
|
const lv_font_t* f = is_ultra ? resolve_verse_font_ultra(ctx) : resolve_verse_font(ctx, font_sz);
|
||||||
|
lv_obj_set_style_text_font(lbl_verse, f, 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
## v6 black-bar crop fix + 15px lift correction
|
||||||
|
Symptom: black bar top cropping Habakkuk 1:6 + "reduce text size for this font" + "main text can be moved a bit up 30px" → corrected "should had been 15".
|
||||||
|
Root cause: header `44px bg #15151F COVER border 1 #232338` at TOP_MID overlapping center_col 90%x80%.
|
||||||
|
Fix:
|
||||||
|
- header `36px` not 44, `bg 0x0E0E14` same as root not 15151F, `TRANSP 0` not COVER, border 0, pad ver 4
|
||||||
|
- ctrl bar `TRANSP 0` + `bg 0x0E0E14`
|
||||||
|
- center `90%x82% align CENTER 0,8` → `0,-22` (30px up) → `0,-7` (15px up: +8 base -15) per user correction
|
||||||
|
- verse `pad_all 4` not 6
|
||||||
|
Vision: Hab 2:9 "No extra black bar overlapping, fully visible", Zeph 1:3 "balanced now, optical center correct, 1.5x padding above controls" PASS. Screenshots 5336 bytes serif vs 3991 sans, 8683 with chrome (menu visible) vs 6882 immersive.
|
||||||
|
|
||||||
|
## Verification v6 final beautiful
|
||||||
|
- Build 4.3MB .app ELF 0 missing `nm -D` vs `symbols.c` exports, 0 undef LVGL
|
||||||
|
- Install .129 0.8.0-dev: heap 46215-47591 free largest 20480 psram 4810396 stable vs pre 47855 → fonts PSRAM not heap
|
||||||
|
- Screenshots: 8683 menu show (top Zeph 1:3 / 31094 + bottom pills X/List/Prev/Pause/Next "premium cinematic Audible-like") → 6882 immersive, serif italic vision PASS Hab 1:17 & Zeph 1:3
|
||||||
|
- Fonts fallback works on 0.7 fleet without C files
|
||||||
|
|
||||||
|
## Auto-hide menu + book browser animation v6 final (see immersive-ui.md detail)
|
||||||
|
|
||||||
|
## Tradeoffs
|
||||||
|
- PSRAM 315KB .rodata fine (4.8MB free), heap delta 0
|
||||||
|
- Curly quotes 0x2018-0x201D included for “The Lord is my shepherd;” reference image
|
||||||
|
- Cormorant Garamond download failed via curl HTML 404, Georgia system font editorial highly legible reverent, user approved beautiful
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
# SDK mismatch & missing-symbol crash – PocketDungeon July 16 2026
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
Board `192.168.68.112` on `{"cpuFamily":"esp32s3","osVersion":"0.8.0-dev"}` shows error dialog:
|
||||||
|
```
|
||||||
|
Error
|
||||||
|
Application failed to start: missing symbol
|
||||||
|
[OK]
|
||||||
|
```
|
||||||
|
Screenshot via `curl http://192.168.68.112/api/screenshot -o /tmp/shot.png` -> PNG 320x240 palettized.
|
||||||
|
|
||||||
|
## Root cause (2-fold)
|
||||||
|
|
||||||
|
1. **SDK / OS version mismatch**
|
||||||
|
- `manifest.properties` said `sdk=0.7.0-dev`
|
||||||
|
- Board runs `0.8.0-dev` (checked `GET /info` and `GET /api/screenshot` header)
|
||||||
|
- Compiled against `0.7.0-dev-esp32s3/TactilitySDK` but device exports differ.
|
||||||
|
|
||||||
|
2. **Unexported LVGL style API used**
|
||||||
|
- Code added for less-cramped QVGA intro used:
|
||||||
|
- `lv_obj_set_style_max_width(...)` -> **NOT exported** in 0.7 nor 0.8 (verified via grep firmware `ESP_ELFSYM_EXPORT` / `DEFINE_MODULE_SYMBOL`)
|
||||||
|
- `lv_obj_set_scrollbar_mode(...)` -> version-dependent / not reliably exported
|
||||||
|
- Header exists in SDK's vendored LVGL, so it compiles, but loader fails at runtime.
|
||||||
|
- `check_symbols.py` pattern:
|
||||||
|
```bash
|
||||||
|
source $IDF_PATH/export.sh && xtensa-esp32s3-elf-nm -D build/.../*.app.elf | grep " U "
|
||||||
|
grep -R "ESP_ELFSYM_EXPORT\|DEFINE_MODULE_SYMBOL" firmware/
|
||||||
|
```
|
||||||
|
- Found exactly 1 missing: `lv_obj_set_style_max_width`.
|
||||||
|
|
||||||
|
## Fix checklist
|
||||||
|
|
||||||
|
1. **Query board version first**
|
||||||
|
```bash
|
||||||
|
curl -s http://192.168.68.112:6666/info
|
||||||
|
curl -s http://192.168.68.112/api/screenshot -o /tmp/intro.png
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Bump manifest to match board**
|
||||||
|
```properties
|
||||||
|
[manifest]
|
||||||
|
version=0.1
|
||||||
|
[target]
|
||||||
|
sdk=0.8.0-dev
|
||||||
|
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Avoid unexported LVGL**
|
||||||
|
Replace `set_style_max_width` with fixed percents:
|
||||||
|
```cpp
|
||||||
|
// BAD (unexported)
|
||||||
|
lv_obj_set_style_max_width(obj, 220, LV_PART_MAIN);
|
||||||
|
|
||||||
|
// GOOD – gives side gutters on 320px QVGA
|
||||||
|
lv_obj_set_width(card, LV_PCT(92));
|
||||||
|
lv_obj_set_width(btn, LV_PCT(80));
|
||||||
|
lv_obj_set_width(exitBtn, LV_PCT(70));
|
||||||
|
```
|
||||||
|
For scroll:
|
||||||
|
```cpp
|
||||||
|
// BAD
|
||||||
|
lv_obj_set_scrollbar_mode(area, LV_SCROLLBAR_MODE_AUTO);
|
||||||
|
|
||||||
|
// GOOD – default scroll works, add flag explicitly
|
||||||
|
lv_obj_add_flag(scrollArea, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Rebuild with correct SDK**
|
||||||
|
```bash
|
||||||
|
export TACTILITY_SDK_PATH=/Users/adolforeyna/Projects/Tactility/firmware/release/TactilitySDK
|
||||||
|
rm -rf Apps/PocketDungeon/build/cmake-build-esp32s3
|
||||||
|
PYTHONPATH="" python3 tactility.py Apps/PocketDungeon build esp32s3 --local-sdk
|
||||||
|
# -> Using SDK .../0.8.0-dev-esp32s3/TactilitySDK
|
||||||
|
# -> Building esp32s3 ELF OK
|
||||||
|
# -> Building package OK
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Symbol verification (from app skill)**
|
||||||
|
Run the skill's `check_symbols.py` – should show 89 undefined, 0 missing after fix.
|
||||||
|
|
||||||
|
6. **Install + screenshot verification**
|
||||||
|
```bash
|
||||||
|
PYTHONPATH="" python3 tactility.py Apps/PocketDungeon install 192.168.68.112 esp32s3 --local-sdk
|
||||||
|
PYTHONPATH="" python3 tactility.py Apps/PocketDungeon run 192.168.68.112
|
||||||
|
curl -s http://192.168.68.112/api/screenshot -o /tmp/intro_v3.png
|
||||||
|
# vision analyze: not cramped, airy, no clipping
|
||||||
|
```
|
||||||
|
|
||||||
|
## QVGA 320x240 cramped UI fix applied
|
||||||
|
|
||||||
|
Original cramped (intro.png vision):
|
||||||
|
- GOAL card edge-to-edge, text `Loot gold, survive monsters` clipped at bottom
|
||||||
|
- Buttons 85-90% wide touching edges, 4-8px gaps, Best touching bottom
|
||||||
|
- No outer margin
|
||||||
|
|
||||||
|
Fixed v3 (intro_v3.png vision PASS):
|
||||||
|
- Outer container `pad_all 10`, `pad_row 8`
|
||||||
|
- Cards `PCT 92` width, `SIZE_CONTENT` height, `pad_all 10`, `pad_row 5`, radius 10, bg #1D2030
|
||||||
|
- ScrollArea `PCT 100`, `flex_grow 1`, scrollable flag, flex col pad_row 8
|
||||||
|
- TitleBox pad_row 3, title #F8F3E6, subtitle #9AA0B8
|
||||||
|
- BtnRow `PCT 100`, `SIZE_CONTENT`, flex center, pad_row 8
|
||||||
|
- Primary btn `PCT 80` 40px accent #365CF5 radius 10
|
||||||
|
- Secondary exit `PCT 70` 30px bg #252836 radius 8
|
||||||
|
- Shortened card bodies to fit QVGA: GOAL 1 line, HOW TO PLAY 3 lines, CONTROLS 4 lines
|
||||||
|
- Ends up airy with 12-16px margins, no clipping.
|
||||||
|
|
||||||
|
Keep text short on QVGA; use `LV_LABEL_LONG_WRAP` + `PCT(100)`.
|
||||||
|
|
||||||
|
## Lessons
|
||||||
|
|
||||||
|
- Always probe board osVersion before build
|
||||||
|
- The `/api/screenshot` endpoint (port 80, not 6666) is invaluable for both crash diagnosis (Error dialog) and layout QA without serial
|
||||||
|
- `max_width` and `scrollbar_mode` specifically are trap APIs – prefer PCT widths for side gutters
|
||||||
|
- TactilitySDK release folder contains both 0.7 and 0.8 subfolders – pick matching one via TACTILITY_SDK_PATH + --local-sdk automatically selects subfolder
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Symbol Check — BibleVerse session (updated July 12 v5 pretty font)
|
||||||
|
|
||||||
|
## Tool: undefined symbols vs firmware exports
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pathlib, re, subprocess
|
||||||
|
elf = pathlib.Path("Apps/BibleVerse/build/cmake-build-esp32s3/BibleVerse.app.elf")
|
||||||
|
r = subprocess.run(["zsh","-c", f"source ~/esp/esp-idf/export.sh >/dev/null 2>&1; xtensa-esp32s3-elf-nm -D {elf}"], capture_output=True, text=True)
|
||||||
|
undef = [l.split()[-1] for l in r.stdout.splitlines() if len(l.split())>=2 and l.split()[-2]=='U']
|
||||||
|
|
||||||
|
pat = re.compile(r'(?:ESP_ELFSYM_EXPORT|DEFINE_MODULE_SYMBOL)\s*\(\s*([A-Za-z0-9_]+)\s*\)')
|
||||||
|
exp=set()
|
||||||
|
for fp in pathlib.Path("../firmware").rglob("*.*"):
|
||||||
|
if 'build' in str(fp): continue
|
||||||
|
if fp.suffix not in ('.c','.h','.cpp'): continue
|
||||||
|
try: exp.update(m.group(1) for m in pat.finditer(fp.read_text(errors='ignore')))
|
||||||
|
except: pass
|
||||||
|
missing = [s for s in undef if s not in exp]
|
||||||
|
print(f"undef {len(undef)} missing {missing}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Session results
|
||||||
|
|
||||||
|
- Before fix: `undef 68, missing ['lv_font_montserrat_14']` — source used `&lv_font_montserrat_14`.
|
||||||
|
- After `lvgl_get_text_font(FONT_SIZE_SMALL)`: `undef 68, missing []` ✅
|
||||||
|
- Editorial v4: 0 missing, anim symbols ok, app 4413440 bytes, heap 54KB stable on .129
|
||||||
|
- **v5 pretty font**: Georgia Italic embedded C 26/22/20/18/16 + regular 24 via `lv_font_conv`, 315KB `.rodata` in PSRAM, 0 missing, app 4.3MB, heap `47539 free largest 20480 psram 4810944` stable, screenshot 5336 bytes Habakkuk 1:17 serif italic editorial.
|
||||||
|
- **v6 final beautiful 2142871**: 0 missing, `undef 0`, app 4.3M, heap `46215-47591 free largest 20480 psram 4810396` stable, screenshots 8683 menu visible (top Zeph 1:3 / 31094 + bottom pills) + 6882 immersive hidden, lift -7 15px corrected from -22 30px per user, black bar fixed. Gotcha `lv_timer_t user_data` field incomplete → `t->user_data` invalid → must use `lv_timer_get_user_data(t)` + `lv_timer_set_user_data` (exports 372/373/382/388). Checked `grep timer symbols.c`. Timer used for chrome auto-hide 10s + book browser anim.
|
||||||
|
|
||||||
|
## Known missing table
|
||||||
|
|
||||||
|
| Symbol | Status | Workaround |
|
||||||
|
|---|---|---|
|
||||||
|
| `lv_font_montserrat_14/18/24` | Not exported | `lvgl_get_text_font(FONT_SIZE_SMALL/DEFAULT/LARGE)` OR embedded C serif via `lv_font_conv` → see `pretty-font.md` |
|
||||||
|
| `LV_SYMBOL_STAR` | May be absent | Use `LV_SYMBOL_OK`, `LV_SYMBOL_DUMMY` |
|
||||||
|
| `xTaskGetCurrentTaskHandle` | Not exported | Avoid task identity check; release lock |
|
||||||
|
| `lv_image_set_rotation` | Not exported | `lv_obj_set_style_transform_rotation(obj, deg*10, 0)` |
|
||||||
|
| `cJSON`, `esp_websocket_client` | Static | Vendor source or raw `lwip_socket` APIs |
|
||||||
|
|
||||||
|
### New missing from v2/v3/v4/v5 UI passes
|
||||||
|
|
||||||
|
- `lv_obj_set_style_bg_grad_color/dir/stop` → **Not exported** 0.8.0-dev. Gradient fails `missing symbol`. Use solid `0x0E0E14`.
|
||||||
|
- `LV_OPA_95` → **Not defined** (only 0/10/20/30/40/50/60/70/80/90/100/COVER/TRANSP). Use `COVER` or `90`.
|
||||||
|
- `lv_arc_create`, `lv_arc_set_range/value/bg_angles`, `lv_arc_get_value` → **Not exported on 0.7.0-dev**, added `dff93cb6 Add lv_arc.h symbols (#496)` after 0.7 release. Symptom blue modal `Error / Application failed to start: missing symbol / OK`, heap healthy ~31KB not OOM. Fix horizontal slide using `lv_slider_create` + `lv_indev_get_point/active`. See `book-browser.md`.
|
||||||
|
- `lv_roller_create` → **Not exported** 0.7 nor 0.8.
|
||||||
|
- `lv_indev_get_point/active`, `lv_slider_create/range/value/set_value` → **Exported since 0.6** safe.
|
||||||
|
- `lv_anim_init`, `set_var/values/duration/exec_cb/path_cb/start`, `path_ease_out/linear/ease_in`, `path_ease_in_out` → **Exported since 0.6** safe (checked lines 435-446). Used for editorial fade 220/300ms. Safe for 0.7/0.8 fleet.
|
||||||
|
- `lv_binfont_create/destroy` → **Exported** 0.8 but VFS path fragile — `tt_app_get_assets_path` returns void, not bool (check `buf[0]!='\0'`). Prefer **embedded C** fonts via `LV_FONT_DECLARE` — lives in PSRAM (315KB `.rodata`), no FS needed. See `pretty-font.md`. Old `.fnt` bins 52KB still bundled in `assets/fonts/` but not used.
|
||||||
|
- `lv_font_t` embedded C — no symbol needed, static const.
|
||||||
|
|
||||||
|
## Heap OOM vs missing-symbol distinction
|
||||||
|
|
||||||
|
- **OOM**: `heap free 275B total 310KB largest_block 23`, install `500 failed to create temp directory`, screenshot `500`. Cause 66-button browser 132-264 objs. Fix ~10 obj browser.
|
||||||
|
- **Missing symbol**: `heap free 31779 largest 12288` healthy, install `200 ok`, run `200 ok`, but app shows blue `Error missing symbol` dialog, screenshot `200 2755`. Fix check `symbols.c`.
|
||||||
|
|
||||||
|
## Font scaling v4/v5
|
||||||
|
|
||||||
|
Only 3 sizes via `lvgl_get_text_font()` fallback, but v5 uses embedded serif as primary:
|
||||||
|
|
||||||
|
```c
|
||||||
|
size_t len = strlen(verse);
|
||||||
|
enum LvglFontSize fs; int line_sp, pad_v, pad_h;
|
||||||
|
if (len < 40) { fs=FONT_SIZE_LARGE; line_sp=14; pad_v=10; pad_h=28; letter_sp=1; } // v4 tightened
|
||||||
|
else if (len <100) { fs=FONT_SIZE_LARGE; line_sp=10; pad_v=8; pad_h=24; }
|
||||||
|
else if (len <200) { fs=FONT_SIZE_DEFAULT; line_sp=8; pad_v=6; pad_h=20; }
|
||||||
|
else if (len <350) { fs=FONT_SIZE_SMALL; line_sp=6; pad_v=4; pad_h=16; }
|
||||||
|
else { fs=FONT_SIZE_SMALL; line_sp=4; pad_v=2; pad_h=12; } // Esther 492c
|
||||||
|
|
||||||
|
// v5: resolve to embedded Georgia Italic 26/20/16 else fallback
|
||||||
|
lv_obj_set_style_text_font(lbl_verse, resolve_verse_font(ctx, fs), 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
Proven .112 Genesis 2:10 77c LARGE, .129 Ephesians 1:4 4138 bytes screenshot editorial warm `#F2F0E8` + ref uppercase tracking 2 `#9A9AA8` gap 10, v5 Habakkuk 1:17 5336 bytes serif italic editorial vision PASS.
|
||||||
|
|
||||||
|
Exported: `lvgl_get_text_font`, `get_text_font_height`, `get_shared_icon_font`, `set_style_text_font`, `timer_create`, `label_set_text`, `text_line_space`, `pad_*`, `text_opa`, `anim_*`, `LV_FONT_DECLARE` (macro, not symbol).
|
||||||
|
|
||||||
|
CI: fail if missing>0, heap stable >40KB with fonts.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Verify ELF symbols against firmware exports. Rerunnable from skill.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 scripts/verify_symbols.py [--elf Apps/BibleVerse/build/cmake-build-esp32s3/BibleVerse.app.elf] [--firmware ../firmware]
|
||||||
|
|
||||||
|
Requires: xtensa-esp32s3-elf-nm in PATH after sourcing ~/esp/esp-idf/export.sh
|
||||||
|
"""
|
||||||
|
import argparse, pathlib, re, subprocess, sys
|
||||||
|
|
||||||
|
def get_undef(elf_path: pathlib.Path):
|
||||||
|
r = subprocess.run(
|
||||||
|
["zsh","-c", f"source /Users/adolforeyna/esp/esp-idf/export.sh >/dev/null 2>&1; xtensa-esp32s3-elf-nm -D {elf_path}"],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
undef=[]
|
||||||
|
for line in r.stdout.splitlines():
|
||||||
|
parts=line.split()
|
||||||
|
if len(parts)>=2 and parts[-2]=='U':
|
||||||
|
undef.append(parts[-1])
|
||||||
|
return undef, r.stdout
|
||||||
|
|
||||||
|
def get_exports(firmware_dir: pathlib.Path):
|
||||||
|
pat=re.compile(r'(?:ESP_ELFSYM_EXPORT|DEFINE_MODULE_SYMBOL)\s*\(\s*([A-Za-z0-9_]+)\s*\)')
|
||||||
|
exp=set()
|
||||||
|
for fp in firmware_dir.rglob("*.*"):
|
||||||
|
if 'build' in str(fp): continue
|
||||||
|
if fp.suffix not in ('.c','.h','.cpp'): continue
|
||||||
|
try:
|
||||||
|
exp.update(m.group(1) for m in pat.finditer(fp.read_text(errors='ignore')))
|
||||||
|
except: pass
|
||||||
|
return exp
|
||||||
|
|
||||||
|
def main():
|
||||||
|
p=argparse.ArgumentParser()
|
||||||
|
p.add_argument('--elf', default='Apps/BibleVerse/build/cmake-build-esp32s3/BibleVerse.app.elf')
|
||||||
|
p.add_argument('--firmware', default='../firmware')
|
||||||
|
args=p.parse_args()
|
||||||
|
elf=pathlib.Path(args.elf)
|
||||||
|
fw=pathlib.Path(args.firmware)
|
||||||
|
if not elf.exists():
|
||||||
|
print(f"ELF not found: {elf}", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
undef, raw = get_undef(elf)
|
||||||
|
exp = get_exports(fw)
|
||||||
|
missing=[s for s in undef if s not in exp]
|
||||||
|
print(f"Undefined: {len(undef)}")
|
||||||
|
print(f"Exported: {len(exp)}")
|
||||||
|
if missing:
|
||||||
|
print(f"Missing {len(missing)}:")
|
||||||
|
for s in sorted(missing):
|
||||||
|
print(f" - {s}")
|
||||||
|
sys.exit(1)
|
||||||
|
print("✅ All symbols resolved")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
name: Publish Apps
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
sdk_version:
|
||||||
|
description: The SDK version that determines the path on the CDN
|
||||||
|
required: true
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: 'Download cdn-files'
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: 'cdn-files'
|
||||||
|
path: cdn_files
|
||||||
|
- name: 'Install boto3'
|
||||||
|
shell: bash
|
||||||
|
run: pip install boto3
|
||||||
|
- name: 'Upload files'
|
||||||
|
shell: bash
|
||||||
|
run: python Buildscripts/CDN/upload-app-files.py cdn_files ${{ inputs.sdk_version }} ${{ env.CDN_ID }} ${{ env.CDN_TOKEN_NAME }} ${{ env.CDN_TOKEN_VALUE }}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
name: Release Apps
|
name: Release Apps
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
sdk_version:
|
||||||
|
description: 'Common SDK version shared by all bundled apps'
|
||||||
|
value: ${{ steps.release.outputs.sdk_version }}
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: 'composite'
|
using: 'composite'
|
||||||
steps:
|
steps:
|
||||||
@@ -11,8 +16,11 @@ runs:
|
|||||||
run: rsync -av downloaded_apps/*/*.app cdn_files/
|
run: rsync -av downloaded_apps/*/*.app cdn_files/
|
||||||
shell: bash
|
shell: bash
|
||||||
- name: 'Create CDN release files'
|
- name: 'Create CDN release files'
|
||||||
run: python release.py cdn_files/
|
id: release
|
||||||
shell: bash
|
shell: bash
|
||||||
|
run: |
|
||||||
|
python release.py cdn_files/
|
||||||
|
echo "sdk_version=$(cat sdk_version.txt)" >> "$GITHUB_OUTPUT"
|
||||||
- name: 'Upload Artifact'
|
- name: 'Upload Artifact'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ jobs:
|
|||||||
Build:
|
Build:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
app_name: [Brainfuck, Breakout, BookPlayer, Calculator, Diceware, EpubReader, GPIO, GraphicsDemo, HelloWorld, M5UnitTest, Magic8Ball, MediaKeys, MystifyDemo, SerialConsole, Snake, TamaTac, TodoList, TwoEleven]
|
app_name: [AudioTest, BibleVerse, BookPlayer, Brainfuck, Breakout, Calculator, Diceware, EpubReader, EspNowBridge, GPIO, GameBoy, GraphicsDemo, HelloWorld, M5UnitTest, Magic8Ball, McpScreen, MediaKeys, Mp3Player, MystifyDemo, PocketDungeon, ReynaBot, RobotArm, SerialConsole, Snake, TamaTac, TodoList, TwoEleven, VoiceRecorder]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: "Build"
|
- name: "Build"
|
||||||
uses: ./.github/actions/build-app
|
uses: ./.github/actions/build-app
|
||||||
with:
|
with:
|
||||||
@@ -23,7 +25,26 @@ jobs:
|
|||||||
Bundle:
|
Bundle:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [Build]
|
needs: [Build]
|
||||||
|
outputs:
|
||||||
|
sdk_version: ${{ steps.release.outputs.sdk_version }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: "Build"
|
- name: "Build"
|
||||||
|
id: release
|
||||||
uses: ./.github/actions/release-apps
|
uses: ./.github/actions/release-apps
|
||||||
|
PublishApps:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [Bundle]
|
||||||
|
if: (github.event_name == 'push' && github.ref == 'refs/heads/main')
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- name: "Publish Apps"
|
||||||
|
env:
|
||||||
|
CDN_ID: ${{ secrets.CDN_ID }}
|
||||||
|
CDN_TOKEN_NAME: ${{ secrets.CDN_TOKEN_NAME }}
|
||||||
|
CDN_TOKEN_VALUE: ${{ secrets.CDN_TOKEN_VALUE }}
|
||||||
|
uses: ./.github/actions/publish-apps
|
||||||
|
with:
|
||||||
|
sdk_version: ${{ needs.Bundle.outputs.sdk_version }}
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# AGENTS.md — Tactility Apps
|
||||||
|
|
||||||
|
This file is for AI agents (and humans) working in this repo. It documents local workflow, owned hardware, and in-repo skills.
|
||||||
|
|
||||||
|
## Repo
|
||||||
|
|
||||||
|
Tactility external ELF apps — loaded at runtime via firmware symbol table. Each app: `Apps/<Name>/manifest.properties` + `main/CMakeLists.txt` + `Source/*.c`.
|
||||||
|
|
||||||
|
Upstream: `https://github.com/TactilityProject/TactilityApps`
|
||||||
|
Personal Gitea mirror: `https://git.reynafamily.com/adolforeyna/tactility_apps` (`personal` remote)
|
||||||
|
|
||||||
|
Dev board IP: `192.168.68.112` (S3, OS 0.8.0-dev, SDK 0.8.0-dev) — main deploy target. `.111` also online. OS image is 0.8.0-dev so manifest should use `sdk=0.8.0-dev` or loader warns.
|
||||||
|
|
||||||
|
## Local Workstation
|
||||||
|
|
||||||
|
- Host: M2 Air (14,2) macOS 26.5.2
|
||||||
|
- Project: `/Users/adolforeyna/Projects/Tactility/apps`
|
||||||
|
- ESP-IDF: `/Users/adolforeyna/esp/esp-idf`, Python env `idf5.3_py3.9_env` at `~/.espressif/python_env/idf5.3_py3.9_env`
|
||||||
|
- Tactility SDK: `/Users/adolforeyna/Projects/Tactility/firmware/release/TactilitySDK` (built locally, `--local-sdk`)
|
||||||
|
|
||||||
|
### Hardware
|
||||||
|
|
||||||
|
| Board | IP | Port | Notes |
|
||||||
|
|-------|-----|------|-------|
|
||||||
|
| ES3C28P 2.8" color 240x320 | `192.168.68.112` | `/dev/cu.usbmodem101` | Primary deploy target, 16MB flash, OCT PSRAM, SD for ELF assets |
|
||||||
|
| Waveshare RLCD 4.2" mono 400x300 ST7305 | `192.168.68.111`? .1101 USB | `/dev/cu.usbmodem1101` | Secondary, compact density, mono threshold 60, font 18 |
|
||||||
|
|
||||||
|
User shorthand: "ending in 112" → `192.168.68.112`.
|
||||||
|
|
||||||
|
## Build, Deploy, Run — Correct Env Wrapper
|
||||||
|
|
||||||
|
Hermes desktop Python 3.11 venv pollutes `PYTHONPATH` → `tactility.py` crashes with `TypeError: unsupported operand type(s) for |` (urllib3 uses `X | Y` 3.10+). Also `idf.py` crashes with `pydantic_core` mismatch.
|
||||||
|
|
||||||
|
Always:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/adolforeyna/Projects/Tactility/apps
|
||||||
|
unset PYTHONPATH; unset PYTHONHOME
|
||||||
|
export IDF_PYTHON_ENV_PATH=/Users/adolforeyna/.espressif/python_env/idf5.3_py3.9_env
|
||||||
|
source /Users/adolforeyna/esp/esp-idf/export.sh
|
||||||
|
export TACTILITY_SDK_PATH=/Users/adolforeyna/Projects/Tactility/firmware/release/TactilitySDK
|
||||||
|
|
||||||
|
# Build
|
||||||
|
$IDF_PYTHON_ENV_PATH/bin/python tactility.py Apps/MyApp clean
|
||||||
|
$IDF_PYTHON_ENV_PATH/bin/python tactility.py Apps/MyApp build esp32s3 --local-sdk --verbose
|
||||||
|
# → build/MyApp.app (tar), build/cmake-build-esp32s3/MyApp.app.elf
|
||||||
|
|
||||||
|
# Symbol check (0 missing = good, any = "Application failed to start: missing symbol" blue OK dialog)
|
||||||
|
xtensa-esp32s3-elf-nm -D Apps/MyApp/build/cmake-build-esp32s3/MyApp.app.elf | grep " U "
|
||||||
|
# or automated:
|
||||||
|
python .claude/skills/tactility-app-development/scripts/verify_symbols.py Apps/MyApp
|
||||||
|
|
||||||
|
# Install to device (dashboard API port 80, not dev port 6666)
|
||||||
|
$IDF_PYTHON_ENV_PATH/bin/python tactility.py Apps/MyApp install 192.168.68.112 esp32s3
|
||||||
|
# or dashboard directly:
|
||||||
|
curl -X PUT http://192.168.68.112/api/apps/install -F "file=@build/MyApp.app"
|
||||||
|
|
||||||
|
# Run
|
||||||
|
curl -X POST "http://192.168.68.112/api/apps/run?id=one.tactility.myapp"
|
||||||
|
# screenshot QA
|
||||||
|
curl -s http://192.168.68.112/api/screenshot -o /tmp/fb.png && open /tmp/fb.png
|
||||||
|
curl http://192.168.68.112/api/apps # list installed
|
||||||
|
|
||||||
|
# Dev server fallback (needs Developer mode enabled in Settings, port 6666)
|
||||||
|
# If 6666 refused → use dashboard PUT above
|
||||||
|
```
|
||||||
|
|
||||||
|
### manifest.properties
|
||||||
|
|
||||||
|
```properties
|
||||||
|
[manifest]
|
||||||
|
version=0.1
|
||||||
|
[target]
|
||||||
|
sdk=0.8.0-dev
|
||||||
|
platforms=esp32s3,esp32p4
|
||||||
|
[app]
|
||||||
|
id=one.tactility.myapp
|
||||||
|
versionName=1.0.0
|
||||||
|
versionCode=1
|
||||||
|
name=My App
|
||||||
|
```
|
||||||
|
|
||||||
|
Always pass `esp32s3` platform arg to `install` when only that ELF is built.
|
||||||
|
|
||||||
|
### CMake Component Template
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
|
||||||
|
# optionally include shared libs:
|
||||||
|
# file(GLOB_RECURSE GAMEKIT Source/*.c* ../../Libraries/GameKit/Source/*.c* ...)
|
||||||
|
idf_component_register(
|
||||||
|
SRCS ${SOURCE_FILES}
|
||||||
|
INCLUDE_DIRS "Source" "../../Libraries/GameKit/Include" ...
|
||||||
|
REQUIRES TactilitySDK esp_driver_i2s ...
|
||||||
|
)
|
||||||
|
target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-format-truncation -Wno-unused-but-set-variable)
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep app source as pure C (no `auto`, no lambdas in `.c`) or rename to `.cpp`.
|
||||||
|
|
||||||
|
### ELF Loader — Missing Symbol Pattern
|
||||||
|
|
||||||
|
Tactility firmware exports symbols via `ESP_ELFSYM_EXPORT` / `DEFINE_MODULE_SYMBOL` in `firmware/Modules/lvgl-module/source/symbols.c`. If ELF references an unexported symbol → device modal `Error / Application failed to start: missing symbol`, heap still healthy (~31KB free) → NOT OOM.
|
||||||
|
|
||||||
|
Known missing on 0.8.0-dev:
|
||||||
|
- `lv_font_montserrat_14/18` → use `lvgl_get_text_font(SMALL/DEFAULT/LARGE)` via `tactility/lvgl_fonts.h`
|
||||||
|
- `lv_obj_set_style_bg_grad_color/dir/stop`, `LV_OPA_95/5`, `LV_SYMBOL_STAR` → use solid colors, COVER/90
|
||||||
|
- `lv_obj_set_style_max_width` → use `LV_PCT(92)` fixed percents for gutters
|
||||||
|
- `lv_obj_set_scrollbar_mode`, `lv_arc_create` (pre PR #496 on 0.7), `lv_roller_create`
|
||||||
|
- `lv_anim_*` → exported since 0.6, safe for 0.7/0.8
|
||||||
|
- `lv_binfont_create` → exported on 0.8.0-dev but VFS fragile, prefer embedded C fonts via `lv_font_conv --format lvgl`
|
||||||
|
|
||||||
|
If OOM not missing-symbol: heap `total 310KB free 275B min_free 23` → 66 buttons case = too many LVGL objects, not PSRAM (ELF lives in PSRAM, LVGL objs internal heap only). Fix: reduce obj count, horizontal slide pattern.
|
||||||
|
|
||||||
|
### App Patterns in This Fleet
|
||||||
|
|
||||||
|
| Pattern | Summary |
|
||||||
|
|---------|---------|
|
||||||
|
| **Immersive full-screen** | Single screen, header+toolbar hidden default, tap toggles chrome, `LV_OBJ_FLAG_HIDDEN`. No `tt_lvgl_toolbar_create_for_app` → saves 40px but need custom exit. |
|
||||||
|
| **QVGA intro layout** | Header + flex row = left scrollable column (GOAL+CONTROLS cards 90% width) + right 44px rail (play 38px accent + close 30px muted icons). Fixes clipped bottom on 320x198 usable. |
|
||||||
|
| **Tutorial teach-by-play** | Dungeon 2-row `TUTORIAL_ROWS=2`, floor 0→@+>, 1→@+b+$+>, real from 2. `isTutorial()` skip monster moves, `renderRows` + `HIDDEN` flag, re-center board. |
|
||||||
|
| **Adaptive scaling** | `apply_verse_scaling()` strlen→ font LARGE/DEFAULT/SMALL + line_space + pad + letter_spacing. Long (>350c) → compact small. |
|
||||||
|
| **Book browser** | Clean slide ~10 objs: [X left][slider flex][N/66 right], center LARGE 50% bigger tappable, swipe 28px threshold, anim 220-300ms via `lv_anim_*`, fade ref name. Avoid 66 buttons → OOM. |
|
||||||
|
| **Asset splitting** | 4.8MB XML bible → per-book `.bin` (NUL-joined verses) + `.idx` (BIBK header u16+u32+offsets) + `books.json`. `tt_app_get_assets_path()` + fallback `/sdcard/bibles/`. |
|
||||||
|
| **Persistence** | `tt_app_get_user_data_child_path(app, "progress.txt", buf, len)` → `snprintf`. Atomic write on change. |
|
||||||
|
| **Exit paths** | (1) `InputKind::Cancel/Menu` Q/ESC → `tt_app_stop()`, (2) X button 36×28, (3) long-press `LV_EVENT_LONG_PRESSED` → pause overlay Resume/Exit. |
|
||||||
|
| **GameKit Input** | `attachInputToCurrent()` — re-attach `GameKit::attachInput(root, scene)` after container changes, non-clickable info boxes so taps bubble, `CLICKABLE|GESTURE_BUBBLE` on containers, stop_bubbling on start button. |
|
||||||
|
| **Screenshot rate-limit** | `/api/screenshot` 2s+ between calls or `ConnectionResetError 54` heap spike. |
|
||||||
|
|
||||||
|
## In-Repo Skill
|
||||||
|
|
||||||
|
| Skill | When to use |
|
||||||
|
|-------|-------------|
|
||||||
|
| `tactility-app-development` | ELF app build/env wrapper, missing-symbol loader triage, asset packaging (per-book binary), immersive LVGL UI, toolbar-less app exit, GameKit/GameInput bubbling pitfall, screenshot rate-limit, QVGA intro rail vs wide buttons, tutorial level pattern, audio apps RLCD pin conflict |
|
||||||
|
|
||||||
|
### Key references
|
||||||
|
|
||||||
|
- `tactility-app-development/references/build-env.md` — Hermes venv PYTHONPATH error transcript + fix
|
||||||
|
- `tactility-app-development/references/symbols.md` — exported vs missing list, OOM vs missing-symbol diagnosis, heap sizes
|
||||||
|
- `tactility-app-development/references/deploy.md` — dashboard vs :6666 install, PUT vs POST, platform arg
|
||||||
|
- `tactility-app-development/references/immersive-ui.md` — editorial v4 warm #F2F0E8, gap 10, fade 220/300ms
|
||||||
|
- `tactility-app-development/references/intro-fullscreen.md` — fullscreen no-toolbar, GameKit bubbling fix, QVGA rail, v7 GOAL+CONTROLS + icons
|
||||||
|
- `tactility-app-development/references/pocket-dungeon.md` — GameKit+SfxEngine GLOB_RECURSE, 112 discovery, 9×7 grid, tutorial 2-row
|
||||||
|
- `tactility-app-development/references/black-bar-crop.md` — header 44→36 TRANSP, Georgia down-tier LARGE→22, 15px lift
|
||||||
|
- `tactility-app-development/references/sdk-mismatch-missing-symbol.md` — sdk bump + `max_width` removal
|
||||||
|
- `tactility-app-development/scripts/verify_symbols.py` — undefined symbol checker
|
||||||
|
|
||||||
|
Load via `skill_view(name='tactility-app-development')` in agent sessions.
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
[manifest]
|
manifest.version=0.2
|
||||||
version=0.1
|
target.sdk=0.8.0-dev
|
||||||
[target]
|
target.platforms=esp32s3
|
||||||
sdk=0.7.0-dev
|
app.id=one.tactility.audiotest
|
||||||
platforms=esp32s3
|
app.version.name=0.1.0
|
||||||
[app]
|
app.version.code=1
|
||||||
id=one.tactility.audiotest
|
app.name=Audio Test
|
||||||
versionName=0.1.0
|
|
||||||
versionCode=1
|
|
||||||
name=Audio Test
|
|
||||||
|
|||||||
@@ -31,9 +31,10 @@ LV_FONT_DECLARE(georgia_regular_24)
|
|||||||
// books.json : array of {bnumber, bname, verses, chapters}
|
// books.json : array of {bnumber, bname, verses, chapters}
|
||||||
|
|
||||||
#define MAX_BIBLE_BOOKS 66
|
#define MAX_BIBLE_BOOKS 66
|
||||||
|
#define MAX_FAVORITES 512
|
||||||
#define MAX_BOOK_NAME 40
|
#define MAX_BOOK_NAME 40
|
||||||
#define MAX_VERSE_TEXT 2048
|
#define MAX_VERSE_TEXT 2048
|
||||||
#define MAX_PATH 320
|
#define MAX_PATH 128
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint32_t offset;
|
uint32_t offset;
|
||||||
@@ -115,11 +116,23 @@ typedef struct {
|
|||||||
lv_obj_t* book_center_num;
|
lv_obj_t* book_center_num;
|
||||||
lv_obj_t* book_center_verses;
|
lv_obj_t* book_center_verses;
|
||||||
lv_obj_t* book_slider; // fast scrub
|
lv_obj_t* book_slider; // fast scrub
|
||||||
|
lv_obj_t* btn_favorites;
|
||||||
int book_dial_idx; // 0..book_count-1 currently selected
|
int book_dial_idx; // 0..book_count-1 currently selected
|
||||||
|
int favorite_indices[MAX_FAVORITES];
|
||||||
|
int favorite_count;
|
||||||
|
int favorite_dial_idx;
|
||||||
|
bool browsing_favorites;
|
||||||
|
bool favorite_navigation_active;
|
||||||
int book_drag_start_x;
|
int book_drag_start_x;
|
||||||
bool book_dragging;
|
bool book_dragging;
|
||||||
bool book_browser_visible;
|
bool book_browser_visible;
|
||||||
|
|
||||||
|
// Reading-view swipe tracking
|
||||||
|
int verse_drag_start_x;
|
||||||
|
int verse_drag_start_y;
|
||||||
|
bool verse_dragging;
|
||||||
|
bool verse_swipe_consumed;
|
||||||
|
|
||||||
// Timer
|
// Timer
|
||||||
lv_timer_t* auto_timer;
|
lv_timer_t* auto_timer;
|
||||||
lv_timer_t* chrome_auto_hide_timer; // menu auto-hide after 10s on start
|
lv_timer_t* chrome_auto_hide_timer; // menu auto-hide after 10s on start
|
||||||
@@ -135,6 +148,7 @@ static bool load_book(AppCtx* ctx, int bnumber);
|
|||||||
static void unload_book(AppCtx* ctx);
|
static void unload_book(AppCtx* ctx);
|
||||||
static void show_current_verse(AppCtx* ctx);
|
static void show_current_verse(AppCtx* ctx);
|
||||||
static void advance_verse(AppCtx* ctx, int delta);
|
static void advance_verse(AppCtx* ctx, int delta);
|
||||||
|
static void manually_advance_verse(AppCtx* ctx, int delta);
|
||||||
static void jump_to_book(AppCtx* ctx, int book_idx);
|
static void jump_to_book(AppCtx* ctx, int book_idx);
|
||||||
static void jump_to_global(AppCtx* ctx, int global_idx);
|
static void jump_to_global(AppCtx* ctx, int global_idx);
|
||||||
static void save_progress(AppCtx* ctx);
|
static void save_progress(AppCtx* ctx);
|
||||||
@@ -142,6 +156,7 @@ static bool load_progress(AppCtx* ctx);
|
|||||||
static void toggle_ui(AppCtx* ctx);
|
static void toggle_ui(AppCtx* ctx);
|
||||||
static void set_chrome_visible(AppCtx* ctx, bool visible);
|
static void set_chrome_visible(AppCtx* ctx, bool visible);
|
||||||
static bool is_favorite(AppCtx* ctx, int global_idx);
|
static bool is_favorite(AppCtx* ctx, int global_idx);
|
||||||
|
static void load_favorites(AppCtx* ctx);
|
||||||
static void toggle_favorite(AppCtx* ctx);
|
static void toggle_favorite(AppCtx* ctx);
|
||||||
static void ensure_user_dir(AppCtx* ctx);
|
static void ensure_user_dir(AppCtx* ctx);
|
||||||
static void set_book_browser_visible(AppCtx* ctx, bool visible);
|
static void set_book_browser_visible(AppCtx* ctx, bool visible);
|
||||||
@@ -239,7 +254,7 @@ static bool parse_books_json(AppCtx* ctx, const char* json_str) {
|
|||||||
const char* obj_end = strchr(obj_start, '}');
|
const char* obj_end = strchr(obj_start, '}');
|
||||||
if (!obj_end) break;
|
if (!obj_end) break;
|
||||||
// parse fields within obj_start..obj_end
|
// parse fields within obj_start..obj_end
|
||||||
char temp[512];
|
char temp[256];
|
||||||
size_t len = (size_t)(obj_end - obj_start + 1);
|
size_t len = (size_t)(obj_end - obj_start + 1);
|
||||||
if (len >= sizeof(temp)) len = sizeof(temp)-1;
|
if (len >= sizeof(temp)) len = sizeof(temp)-1;
|
||||||
memcpy(temp, obj_start, len);
|
memcpy(temp, obj_start, len);
|
||||||
@@ -524,13 +539,33 @@ static bool is_favorite(AppCtx* ctx, int global_idx) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void load_favorites(AppCtx* ctx) {
|
||||||
|
ctx->favorite_count = 0;
|
||||||
|
FILE* f = fopen(ctx->favorites_path, "r");
|
||||||
|
if (!f) return;
|
||||||
|
int global_idx;
|
||||||
|
while (ctx->favorite_count < MAX_FAVORITES && fscanf(f, "%d", &global_idx) == 1) {
|
||||||
|
if (global_idx >= 0 && global_idx < ctx->total_verses) {
|
||||||
|
ctx->favorite_indices[ctx->favorite_count++] = global_idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int find_favorite_position(AppCtx* ctx, int global_idx) {
|
||||||
|
for (int i = 0; i < ctx->favorite_count; i++) {
|
||||||
|
if (ctx->favorite_indices[i] == global_idx) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
static void toggle_favorite(AppCtx* ctx) {
|
static void toggle_favorite(AppCtx* ctx) {
|
||||||
// load all
|
// load all
|
||||||
int vals[512];
|
int vals[MAX_FAVORITES];
|
||||||
int cnt=0;
|
int cnt=0;
|
||||||
FILE* f = fopen(ctx->favorites_path, "r");
|
FILE* f = fopen(ctx->favorites_path, "r");
|
||||||
if (f) {
|
if (f) {
|
||||||
while (cnt<512 && fscanf(f, "%d", &vals[cnt])==1) cnt++;
|
while (cnt<MAX_FAVORITES && fscanf(f, "%d", &vals[cnt])==1) cnt++;
|
||||||
fclose(f);
|
fclose(f);
|
||||||
}
|
}
|
||||||
// check existence
|
// check existence
|
||||||
@@ -542,7 +577,7 @@ static void toggle_favorite(AppCtx* ctx) {
|
|||||||
cnt--;
|
cnt--;
|
||||||
ctx->is_highlighted=false;
|
ctx->is_highlighted=false;
|
||||||
} else {
|
} else {
|
||||||
if (cnt<512) {
|
if (cnt<MAX_FAVORITES) {
|
||||||
vals[cnt++]=ctx->cur_global;
|
vals[cnt++]=ctx->cur_global;
|
||||||
ctx->is_highlighted=true;
|
ctx->is_highlighted=true;
|
||||||
}
|
}
|
||||||
@@ -552,6 +587,7 @@ static void toggle_favorite(AppCtx* ctx) {
|
|||||||
if (!f) return;
|
if (!f) return;
|
||||||
for (int i=0;i<cnt;i++) fprintf(f, "%d\n", vals[i]);
|
for (int i=0;i<cnt;i++) fprintf(f, "%d\n", vals[i]);
|
||||||
fclose(f);
|
fclose(f);
|
||||||
|
load_favorites(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── UI logic ── */
|
/* ── UI logic ── */
|
||||||
@@ -672,27 +708,27 @@ static void show_current_verse(AppCtx* ctx) {
|
|||||||
const char* text_ptr = "(empty)";
|
const char* text_ptr = "(empty)";
|
||||||
if (off < ctx->book_bin_size) text_ptr = (const char*)(ctx->book_bin + off);
|
if (off < ctx->book_bin_size) text_ptr = (const char*)(ctx->book_bin + off);
|
||||||
|
|
||||||
char verse_buf[MAX_VERSE_TEXT];
|
// Fix stack overflow: verse_buf was 2048 bytes on stack, causing gui task (4096) overflow
|
||||||
strncpy(verse_buf, text_ptr, sizeof(verse_buf)-1);
|
// Use heap allocation
|
||||||
verse_buf[sizeof(verse_buf)-1]='\0';
|
char* verse_buf = (char*)malloc(MAX_VERSE_TEXT);
|
||||||
|
if (!verse_buf) {
|
||||||
|
ESP_LOGE(TAG, "Failed to malloc verse_buf");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
strncpy(verse_buf, text_ptr, MAX_VERSE_TEXT-1);
|
||||||
|
verse_buf[MAX_VERSE_TEXT-1]='\0';
|
||||||
|
|
||||||
char ref_buf[96];
|
char ref_buf[64];
|
||||||
snprintf(ref_buf, sizeof(ref_buf), "%s %d:%d", ctx->books[ctx->cur_book_idx].bname, vi->cnum, vi->vnum);
|
snprintf(ref_buf, sizeof(ref_buf), "%s %d:%d", ctx->books[ctx->cur_book_idx].bname, vi->cnum, vi->vnum);
|
||||||
|
|
||||||
// Editorial reference style like your image: "PSALMS 23:1" with tracking
|
char ref_pretty[80];
|
||||||
char ref_pretty[108];
|
char bname_upper[40];
|
||||||
// Use uppercase for book? Keep title for now but uppercase style for editorial match
|
|
||||||
// We'll format as "— PSALMS 23:1 —" feel? For dark mode keep minimal like ref image
|
|
||||||
// Reference image shows lines flanking citation - we do with letterspacing + dim
|
|
||||||
// For now: "PSALMS 23:1" style - uppercase via runtime? We'll uppercase book name
|
|
||||||
char bname_upper[48];
|
|
||||||
strncpy(bname_upper, ctx->books[ctx->cur_book_idx].bname, sizeof(bname_upper)-1);
|
strncpy(bname_upper, ctx->books[ctx->cur_book_idx].bname, sizeof(bname_upper)-1);
|
||||||
bname_upper[sizeof(bname_upper)-1]='\0';
|
bname_upper[sizeof(bname_upper)-1]='\0';
|
||||||
for (char* p=bname_upper; *p; ++p) *p = toupper((unsigned char)*p);
|
for (char* p=bname_upper; *p; ++p) *p = toupper((unsigned char)*p);
|
||||||
snprintf(ref_pretty, sizeof(ref_pretty), "%s %d:%d", bname_upper, vi->cnum, vi->vnum);
|
snprintf(ref_pretty, sizeof(ref_pretty), "%s %d:%d", bname_upper, vi->cnum, vi->vnum);
|
||||||
|
|
||||||
if (ctx->lbl_verse) {
|
if (ctx->lbl_verse) {
|
||||||
// start faded for transition
|
|
||||||
lv_obj_set_style_text_opa(ctx->lbl_verse, 60, 0);
|
lv_obj_set_style_text_opa(ctx->lbl_verse, 60, 0);
|
||||||
lv_label_set_text(ctx->lbl_verse, verse_buf);
|
lv_label_set_text(ctx->lbl_verse, verse_buf);
|
||||||
apply_verse_scaling(ctx, verse_buf);
|
apply_verse_scaling(ctx, verse_buf);
|
||||||
@@ -709,9 +745,24 @@ static void show_current_verse(AppCtx* ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx->is_highlighted = is_favorite(ctx, ctx->cur_global);
|
ctx->is_highlighted = is_favorite(ctx, ctx->cur_global);
|
||||||
|
if (ctx->favorite_navigation_active) {
|
||||||
|
load_favorites(ctx);
|
||||||
|
int favorite_pos = find_favorite_position(ctx, ctx->cur_global);
|
||||||
|
if (favorite_pos >= 0) {
|
||||||
|
ctx->favorite_dial_idx = favorite_pos;
|
||||||
|
if (ctx->lbl_position) {
|
||||||
|
char favorite_pos_buf[40];
|
||||||
|
snprintf(favorite_pos_buf, sizeof(favorite_pos_buf), "Fav %d / %d",
|
||||||
|
favorite_pos + 1, ctx->favorite_count);
|
||||||
|
lv_label_set_text(ctx->lbl_position, favorite_pos_buf);
|
||||||
|
}
|
||||||
|
} else if (ctx->favorite_count == 0) {
|
||||||
|
ctx->favorite_navigation_active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (ctx->btn_highlight) {
|
if (ctx->btn_highlight) {
|
||||||
lv_obj_t* lbl = lv_obj_get_child(ctx->btn_highlight, 0);
|
lv_obj_t* lbl = lv_obj_get_child(ctx->btn_highlight, 0);
|
||||||
if (lbl) lv_label_set_text(lbl, ctx->is_highlighted ? LV_SYMBOL_OK : LV_SYMBOL_DUMMY);
|
if (lbl) lv_label_set_text(lbl, ctx->is_highlighted ? LV_SYMBOL_OK : LV_SYMBOL_PLUS);
|
||||||
if (ctx->is_highlighted) {
|
if (ctx->is_highlighted) {
|
||||||
lv_obj_set_style_bg_color(ctx->btn_highlight, lv_color_hex(0xF5C16C), 0);
|
lv_obj_set_style_bg_color(ctx->btn_highlight, lv_color_hex(0xF5C16C), 0);
|
||||||
lv_obj_set_style_text_color(ctx->btn_highlight, lv_color_hex(0x1E1E2E), 0);
|
lv_obj_set_style_text_color(ctx->btn_highlight, lv_color_hex(0x1E1E2E), 0);
|
||||||
@@ -734,18 +785,47 @@ static void show_current_verse(AppCtx* ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ctx->btn_prev) {
|
if (ctx->btn_prev) {
|
||||||
if (ctx->cur_global<=0) lv_obj_add_state(ctx->btn_prev, LV_STATE_DISABLED);
|
bool at_start = ctx->favorite_navigation_active
|
||||||
|
? ctx->favorite_dial_idx <= 0
|
||||||
|
: ctx->cur_global <= 0;
|
||||||
|
if (at_start) lv_obj_add_state(ctx->btn_prev, LV_STATE_DISABLED);
|
||||||
else lv_obj_clear_state(ctx->btn_prev, LV_STATE_DISABLED);
|
else lv_obj_clear_state(ctx->btn_prev, LV_STATE_DISABLED);
|
||||||
}
|
}
|
||||||
if (ctx->btn_next) {
|
if (ctx->btn_next) {
|
||||||
if (ctx->cur_global >= ctx->total_verses-1) lv_obj_add_state(ctx->btn_next, LV_STATE_DISABLED);
|
bool at_end = ctx->favorite_navigation_active
|
||||||
|
? ctx->favorite_dial_idx >= ctx->favorite_count - 1
|
||||||
|
: ctx->cur_global >= ctx->total_verses - 1;
|
||||||
|
if (at_end) lv_obj_add_state(ctx->btn_next, LV_STATE_DISABLED);
|
||||||
else lv_obj_clear_state(ctx->btn_next, LV_STATE_DISABLED);
|
else lv_obj_clear_state(ctx->btn_next, LV_STATE_DISABLED);
|
||||||
}
|
}
|
||||||
|
free(verse_buf);
|
||||||
save_progress(ctx);
|
save_progress(ctx);
|
||||||
verse_fade_in(ctx);
|
verse_fade_in(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void advance_verse(AppCtx* ctx, int delta) {
|
static void advance_verse(AppCtx* ctx, int delta) {
|
||||||
|
if (ctx->favorite_navigation_active) {
|
||||||
|
load_favorites(ctx);
|
||||||
|
if (ctx->favorite_count <= 0) {
|
||||||
|
ctx->favorite_navigation_active = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int current_pos = find_favorite_position(ctx, ctx->cur_global);
|
||||||
|
if (current_pos < 0) current_pos = ctx->favorite_dial_idx;
|
||||||
|
if (current_pos < 0) current_pos = 0;
|
||||||
|
if (current_pos >= ctx->favorite_count) current_pos = ctx->favorite_count - 1;
|
||||||
|
int next_pos = current_pos + delta;
|
||||||
|
if (next_pos < 0) next_pos = 0;
|
||||||
|
if (next_pos >= ctx->favorite_count) next_pos = ctx->favorite_count - 1;
|
||||||
|
ctx->favorite_dial_idx = next_pos;
|
||||||
|
int next_global = ctx->favorite_indices[next_pos];
|
||||||
|
if (next_global != ctx->cur_global) {
|
||||||
|
ctx->cur_global = next_global;
|
||||||
|
show_current_verse(ctx);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
int ng = ctx->cur_global + delta;
|
int ng = ctx->cur_global + delta;
|
||||||
if (ng < 0) ng = 0;
|
if (ng < 0) ng = 0;
|
||||||
if (ng >= ctx->total_verses) ng = ctx->total_verses-1;
|
if (ng >= ctx->total_verses) ng = ctx->total_verses-1;
|
||||||
@@ -755,6 +835,11 @@ static void advance_verse(AppCtx* ctx, int delta) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void manually_advance_verse(AppCtx* ctx, int delta) {
|
||||||
|
advance_verse(ctx, delta);
|
||||||
|
if (ctx->auto_timer) lv_timer_reset(ctx->auto_timer);
|
||||||
|
}
|
||||||
|
|
||||||
static void jump_to_global(AppCtx* ctx, int global_idx) {
|
static void jump_to_global(AppCtx* ctx, int global_idx) {
|
||||||
if (global_idx < 0) global_idx = 0;
|
if (global_idx < 0) global_idx = 0;
|
||||||
if (global_idx >= ctx->total_verses) global_idx = ctx->total_verses-1;
|
if (global_idx >= ctx->total_verses) global_idx = ctx->total_verses-1;
|
||||||
@@ -768,6 +853,7 @@ static void jump_to_global(AppCtx* ctx, int global_idx) {
|
|||||||
|
|
||||||
static void jump_to_book(AppCtx* ctx, int book_idx) {
|
static void jump_to_book(AppCtx* ctx, int book_idx) {
|
||||||
if (book_idx < 0 || book_idx >= ctx->book_count) return;
|
if (book_idx < 0 || book_idx >= ctx->book_count) return;
|
||||||
|
ctx->favorite_navigation_active = false;
|
||||||
// compute global start of book
|
// compute global start of book
|
||||||
int global = 0;
|
int global = 0;
|
||||||
for (int i=0;i<book_idx;i++) global += ctx->books[i].verse_count;
|
for (int i=0;i<book_idx;i++) global += ctx->books[i].verse_count;
|
||||||
@@ -778,7 +864,10 @@ static void jump_to_book(AppCtx* ctx, int book_idx) {
|
|||||||
static void on_auto_timer(lv_timer_t* t) {
|
static void on_auto_timer(lv_timer_t* t) {
|
||||||
AppCtx* ctx = (AppCtx*)lv_timer_get_user_data(t);
|
AppCtx* ctx = (AppCtx*)lv_timer_get_user_data(t);
|
||||||
if (ctx->auto_paused) return;
|
if (ctx->auto_paused) return;
|
||||||
if (ctx->cur_global < ctx->total_verses-1) {
|
bool has_next = ctx->favorite_navigation_active
|
||||||
|
? ctx->favorite_dial_idx < ctx->favorite_count - 1
|
||||||
|
: ctx->cur_global < ctx->total_verses - 1;
|
||||||
|
if (has_next) {
|
||||||
advance_verse(ctx, 1);
|
advance_verse(ctx, 1);
|
||||||
} else {
|
} else {
|
||||||
ctx->auto_paused = true;
|
ctx->auto_paused = true;
|
||||||
@@ -802,6 +891,51 @@ static void book_fade_anim_cb(void* var, int32_t v) {
|
|||||||
|
|
||||||
static void book_dial_update_center(AppCtx* ctx) {
|
static void book_dial_update_center(AppCtx* ctx) {
|
||||||
if (!ctx->book_center_name) return;
|
if (!ctx->book_center_name) return;
|
||||||
|
if (ctx->browsing_favorites) {
|
||||||
|
if (ctx->favorite_count <= 0) {
|
||||||
|
lv_label_set_text(ctx->book_center_name, "No favorites yet");
|
||||||
|
lv_label_set_text(ctx->book_center_verses, "Mark a verse with the check button, then return here.");
|
||||||
|
lv_label_set_text(ctx->book_center_num, "0 / 0");
|
||||||
|
lv_slider_set_range(ctx->book_slider, 0, 0);
|
||||||
|
lv_slider_set_value(ctx->book_slider, 0, LV_ANIM_OFF);
|
||||||
|
lv_obj_set_style_text_color(ctx->book_center_name, lv_color_hex(0xF5C16C), 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx->favorite_dial_idx < 0) ctx->favorite_dial_idx = 0;
|
||||||
|
if (ctx->favorite_dial_idx >= ctx->favorite_count) ctx->favorite_dial_idx = ctx->favorite_count - 1;
|
||||||
|
int global_idx = ctx->favorite_indices[ctx->favorite_dial_idx];
|
||||||
|
int remaining = global_idx;
|
||||||
|
int book_idx = 0;
|
||||||
|
while (book_idx < ctx->book_count - 1 &&
|
||||||
|
remaining >= ctx->books[book_idx].verse_count) {
|
||||||
|
remaining -= ctx->books[book_idx].verse_count;
|
||||||
|
book_idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* verse_text = "Unable to load verse";
|
||||||
|
char ref[64];
|
||||||
|
snprintf(ref, sizeof(ref), "%s", ctx->books[book_idx].bname);
|
||||||
|
if (load_book(ctx, ctx->books[book_idx].bnumber) &&
|
||||||
|
remaining >= 0 && remaining < ctx->book_idx_count) {
|
||||||
|
VerseIndex* vi = &ctx->book_idx[remaining];
|
||||||
|
snprintf(ref, sizeof(ref), "%s %d:%d", ctx->books[book_idx].bname, vi->cnum, vi->vnum);
|
||||||
|
if (vi->offset < ctx->book_bin_size) {
|
||||||
|
verse_text = (const char*)(ctx->book_bin + vi->offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_label_set_text(ctx->book_center_name, ref);
|
||||||
|
lv_label_set_text(ctx->book_center_verses, verse_text);
|
||||||
|
char count[24];
|
||||||
|
snprintf(count, sizeof(count), "%d / %d", ctx->favorite_dial_idx + 1, ctx->favorite_count);
|
||||||
|
lv_label_set_text(ctx->book_center_num, count);
|
||||||
|
lv_slider_set_range(ctx->book_slider, 0, ctx->favorite_count - 1);
|
||||||
|
lv_slider_set_value(ctx->book_slider, ctx->favorite_dial_idx, LV_ANIM_OFF);
|
||||||
|
lv_obj_set_style_text_color(ctx->book_center_name, lv_color_hex(0xF5C16C), 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (ctx->book_dial_idx <0) ctx->book_dial_idx=0;
|
if (ctx->book_dial_idx <0) ctx->book_dial_idx=0;
|
||||||
if (ctx->book_dial_idx >= ctx->book_count) ctx->book_dial_idx = ctx->book_count-1;
|
if (ctx->book_dial_idx >= ctx->book_count) ctx->book_dial_idx = ctx->book_count-1;
|
||||||
BookInfo* b = &ctx->books[ctx->book_dial_idx];
|
BookInfo* b = &ctx->books[ctx->book_dial_idx];
|
||||||
@@ -834,6 +968,7 @@ static void book_dial_update_center(AppCtx* ctx) {
|
|||||||
lv_obj_set_style_text_color(ctx->book_center_name, lv_color_hex(col), 0);
|
lv_obj_set_style_text_color(ctx->book_center_name, lv_color_hex(col), 0);
|
||||||
|
|
||||||
if (ctx->book_slider) {
|
if (ctx->book_slider) {
|
||||||
|
lv_slider_set_range(ctx->book_slider, 0, ctx->book_count>0 ? ctx->book_count-1 : 0);
|
||||||
// avoid recursive event loop - set without anim, but we add subtle anim for drag
|
// avoid recursive event loop - set without anim, but we add subtle anim for drag
|
||||||
lv_slider_set_value(ctx->book_slider, ctx->book_dial_idx, LV_ANIM_OFF);
|
lv_slider_set_value(ctx->book_slider, ctx->book_dial_idx, LV_ANIM_OFF);
|
||||||
}
|
}
|
||||||
@@ -842,7 +977,13 @@ static void book_dial_update_center(AppCtx* ctx) {
|
|||||||
static void set_book_browser_visible(AppCtx* ctx, bool visible) {
|
static void set_book_browser_visible(AppCtx* ctx, bool visible) {
|
||||||
ctx->book_browser_visible = visible;
|
ctx->book_browser_visible = visible;
|
||||||
if (visible) {
|
if (visible) {
|
||||||
|
ctx->browsing_favorites = false;
|
||||||
ctx->book_dial_idx = ctx->cur_book_idx;
|
ctx->book_dial_idx = ctx->cur_book_idx;
|
||||||
|
load_favorites(ctx);
|
||||||
|
if (ctx->btn_favorites) {
|
||||||
|
lv_obj_set_style_bg_color(ctx->btn_favorites, lv_color_hex(0x252538), 0);
|
||||||
|
lv_obj_set_style_text_color(ctx->btn_favorites, lv_color_hex(0xC4C4D4), 0);
|
||||||
|
}
|
||||||
book_dial_update_center(ctx);
|
book_dial_update_center(ctx);
|
||||||
if (ctx->book_browser) {
|
if (ctx->book_browser) {
|
||||||
lv_obj_remove_flag(ctx->book_browser, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_remove_flag(ctx->book_browser, LV_OBJ_FLAG_HIDDEN);
|
||||||
@@ -889,20 +1030,65 @@ static void on_books_close_click(lv_event_t* e) {
|
|||||||
}
|
}
|
||||||
static void on_book_center_click(lv_event_t* e) {
|
static void on_book_center_click(lv_event_t* e) {
|
||||||
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
||||||
jump_to_book(ctx, ctx->book_dial_idx);
|
if (ctx->browsing_favorites) {
|
||||||
|
if (ctx->favorite_count > 0) {
|
||||||
|
jump_to_global(ctx, ctx->favorite_indices[ctx->favorite_dial_idx]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
jump_to_book(ctx, ctx->book_dial_idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
static void on_dial_prev_click(lv_event_t* e) {
|
static void on_dial_prev_click(lv_event_t* e) {
|
||||||
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
||||||
if (ctx->book_dial_idx>0) { ctx->book_dial_idx--; book_dial_update_center(ctx); }
|
if (ctx->browsing_favorites) {
|
||||||
|
if (ctx->favorite_dial_idx > 0) {
|
||||||
|
ctx->favorite_dial_idx--;
|
||||||
|
book_dial_update_center(ctx);
|
||||||
|
}
|
||||||
|
} else if (ctx->book_dial_idx>0) {
|
||||||
|
ctx->book_dial_idx--;
|
||||||
|
book_dial_update_center(ctx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
static void on_dial_next_click(lv_event_t* e) {
|
static void on_dial_next_click(lv_event_t* e) {
|
||||||
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
||||||
if (ctx->book_dial_idx < ctx->book_count-1) { ctx->book_dial_idx++; book_dial_update_center(ctx); }
|
if (ctx->browsing_favorites) {
|
||||||
|
if (ctx->favorite_dial_idx < ctx->favorite_count - 1) {
|
||||||
|
ctx->favorite_dial_idx++;
|
||||||
|
book_dial_update_center(ctx);
|
||||||
|
}
|
||||||
|
} else if (ctx->book_dial_idx < ctx->book_count-1) {
|
||||||
|
ctx->book_dial_idx++;
|
||||||
|
book_dial_update_center(ctx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
static void on_slider_changed(lv_event_t* e) {
|
static void on_slider_changed(lv_event_t* e) {
|
||||||
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
||||||
int v = lv_slider_get_value(ctx->book_slider);
|
int v = lv_slider_get_value(ctx->book_slider);
|
||||||
if (v!=ctx->book_dial_idx) { ctx->book_dial_idx=v; book_dial_update_center(ctx); }
|
if (ctx->browsing_favorites) {
|
||||||
|
if (v != ctx->favorite_dial_idx) {
|
||||||
|
ctx->favorite_dial_idx = v;
|
||||||
|
book_dial_update_center(ctx);
|
||||||
|
}
|
||||||
|
} else if (v!=ctx->book_dial_idx) {
|
||||||
|
ctx->book_dial_idx=v;
|
||||||
|
book_dial_update_center(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void on_favorites_click(lv_event_t* e) {
|
||||||
|
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
||||||
|
load_favorites(ctx);
|
||||||
|
if (ctx->favorite_count <= 0) {
|
||||||
|
ctx->browsing_favorites = true;
|
||||||
|
book_dial_update_center(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int current_pos = find_favorite_position(ctx, ctx->cur_global);
|
||||||
|
ctx->favorite_dial_idx = current_pos >= 0 ? current_pos : 0;
|
||||||
|
ctx->favorite_navigation_active = true;
|
||||||
|
ctx->browsing_favorites = false;
|
||||||
|
jump_to_global(ctx, ctx->favorite_indices[ctx->favorite_dial_idx]);
|
||||||
}
|
}
|
||||||
static void on_swipe_area_event(lv_event_t* e) {
|
static void on_swipe_area_event(lv_event_t* e) {
|
||||||
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
||||||
@@ -918,11 +1104,15 @@ static void on_swipe_area_event(lv_event_t* e) {
|
|||||||
int thresh = 28; // px per book
|
int thresh = 28; // px per book
|
||||||
if (abs(dx) >= thresh) {
|
if (abs(dx) >= thresh) {
|
||||||
int steps = dx / thresh;
|
int steps = dx / thresh;
|
||||||
int new_idx = ctx->book_dial_idx - steps; // swipe left -> next
|
int old_idx = ctx->browsing_favorites ? ctx->favorite_dial_idx : ctx->book_dial_idx;
|
||||||
|
int max_idx = ctx->browsing_favorites ? ctx->favorite_count - 1 : ctx->book_count - 1;
|
||||||
|
if (max_idx < 0) max_idx = 0;
|
||||||
|
int new_idx = old_idx - steps; // swipe left -> next
|
||||||
if (new_idx<0) new_idx=0;
|
if (new_idx<0) new_idx=0;
|
||||||
if (new_idx>=ctx->book_count) new_idx=ctx->book_count-1;
|
if (new_idx>max_idx) new_idx=max_idx;
|
||||||
if (new_idx!=ctx->book_dial_idx) {
|
if (new_idx!=old_idx) {
|
||||||
ctx->book_dial_idx = new_idx;
|
if (ctx->browsing_favorites) ctx->favorite_dial_idx = new_idx;
|
||||||
|
else ctx->book_dial_idx = new_idx;
|
||||||
book_dial_update_center(ctx);
|
book_dial_update_center(ctx);
|
||||||
}
|
}
|
||||||
ctx->book_drag_start_x = p.x; // reset for next step
|
ctx->book_drag_start_x = p.x; // reset for next step
|
||||||
@@ -991,6 +1181,9 @@ static void build_book_browser(AppCtx* ctx, lv_obj_t* parent) {
|
|||||||
|
|
||||||
ctx->book_center_verses = lv_label_create(mid_wrap);
|
ctx->book_center_verses = lv_label_create(mid_wrap);
|
||||||
lv_label_set_text(ctx->book_center_verses, "—");
|
lv_label_set_text(ctx->book_center_verses, "—");
|
||||||
|
lv_obj_set_width(ctx->book_center_verses, LV_PCT(100));
|
||||||
|
lv_obj_set_height(ctx->book_center_verses, 72);
|
||||||
|
lv_label_set_long_mode(ctx->book_center_verses, LV_LABEL_LONG_MODE_DOTS);
|
||||||
lv_obj_set_style_text_color(ctx->book_center_verses, lv_color_hex(0x5A5A78), 0);
|
lv_obj_set_style_text_color(ctx->book_center_verses, lv_color_hex(0x5A5A78), 0);
|
||||||
lv_obj_set_style_text_font(ctx->book_center_verses, lvgl_get_text_font(FONT_SIZE_SMALL), 0);
|
lv_obj_set_style_text_font(ctx->book_center_verses, lvgl_get_text_font(FONT_SIZE_SMALL), 0);
|
||||||
lv_obj_set_style_text_align(ctx->book_center_verses, LV_TEXT_ALIGN_CENTER, 0);
|
lv_obj_set_style_text_align(ctx->book_center_verses, LV_TEXT_ALIGN_CENTER, 0);
|
||||||
@@ -1004,7 +1197,7 @@ static void build_book_browser(AppCtx* ctx, lv_obj_t* parent) {
|
|||||||
lv_obj_t* l_r = lv_label_create(btn_r); lv_label_set_text(l_r, LV_SYMBOL_NEXT); lv_obj_center(l_r);
|
lv_obj_t* l_r = lv_label_create(btn_r); lv_label_set_text(l_r, LV_SYMBOL_NEXT); lv_obj_center(l_r);
|
||||||
lv_obj_add_event_cb(btn_r, on_dial_next_click, LV_EVENT_CLICKED, ctx);
|
lv_obj_add_event_cb(btn_r, on_dial_next_click, LV_EVENT_CLICKED, ctx);
|
||||||
|
|
||||||
// Bottom bar: [X cancel] [slider] [N / 66]
|
// Bottom bar: [X cancel] [Fav] [slider] [N / 66]
|
||||||
lv_obj_t* bottom = lv_obj_create(ctx->book_browser);
|
lv_obj_t* bottom = lv_obj_create(ctx->book_browser);
|
||||||
lv_obj_set_size(bottom, LV_PCT(100), 56);
|
lv_obj_set_size(bottom, LV_PCT(100), 56);
|
||||||
lv_obj_align(bottom, LV_ALIGN_BOTTOM_MID, 0, 0);
|
lv_obj_align(bottom, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||||
@@ -1028,6 +1221,17 @@ static void build_book_browser(AppCtx* ctx, lv_obj_t* parent) {
|
|||||||
lv_obj_t* l_cancel = lv_label_create(btn_cancel); lv_label_set_text(l_cancel, LV_SYMBOL_CLOSE); lv_obj_center(l_cancel);
|
lv_obj_t* l_cancel = lv_label_create(btn_cancel); lv_label_set_text(l_cancel, LV_SYMBOL_CLOSE); lv_obj_center(l_cancel);
|
||||||
lv_obj_add_event_cb(btn_cancel, on_books_close_click, LV_EVENT_CLICKED, ctx);
|
lv_obj_add_event_cb(btn_cancel, on_books_close_click, LV_EVENT_CLICKED, ctx);
|
||||||
|
|
||||||
|
ctx->btn_favorites = lv_button_create(bottom);
|
||||||
|
lv_obj_set_size(ctx->btn_favorites, 44, 32);
|
||||||
|
lv_obj_set_style_radius(ctx->btn_favorites, 8, 0);
|
||||||
|
lv_obj_set_style_bg_color(ctx->btn_favorites, lv_color_hex(0x252538), 0);
|
||||||
|
lv_obj_set_style_text_color(ctx->btn_favorites, lv_color_hex(0xC4C4D4), 0);
|
||||||
|
lv_obj_t* l_favorites = lv_label_create(ctx->btn_favorites);
|
||||||
|
lv_label_set_text(l_favorites, "Fav");
|
||||||
|
lv_obj_set_style_text_font(l_favorites, lvgl_get_text_font(FONT_SIZE_SMALL), 0);
|
||||||
|
lv_obj_center(l_favorites);
|
||||||
|
lv_obj_add_event_cb(ctx->btn_favorites, on_favorites_click, LV_EVENT_CLICKED, ctx);
|
||||||
|
|
||||||
ctx->book_slider = lv_slider_create(bottom);
|
ctx->book_slider = lv_slider_create(bottom);
|
||||||
lv_obj_set_flex_grow(ctx->book_slider, 1);
|
lv_obj_set_flex_grow(ctx->book_slider, 1);
|
||||||
lv_obj_set_height(ctx->book_slider, 8);
|
lv_obj_set_height(ctx->book_slider, 8);
|
||||||
@@ -1051,8 +1255,35 @@ static void build_book_browser(AppCtx* ctx, lv_obj_t* parent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── UI callbacks ── */
|
/* ── UI callbacks ── */
|
||||||
|
static void on_verse_pointer_event(lv_event_t* e) {
|
||||||
|
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
||||||
|
lv_event_code_t code = lv_event_get_code(e);
|
||||||
|
lv_indev_t* indev = lv_indev_active();
|
||||||
|
if (!indev) return;
|
||||||
|
lv_point_t p;
|
||||||
|
lv_indev_get_point(indev, &p);
|
||||||
|
if (code == LV_EVENT_PRESSED) {
|
||||||
|
ctx->verse_drag_start_x = p.x;
|
||||||
|
ctx->verse_drag_start_y = p.y;
|
||||||
|
ctx->verse_dragging = true;
|
||||||
|
ctx->verse_swipe_consumed = false;
|
||||||
|
} else if ((code == LV_EVENT_RELEASED || code == LV_EVENT_PRESS_LOST) && ctx->verse_dragging) {
|
||||||
|
int dx = p.x - ctx->verse_drag_start_x;
|
||||||
|
int dy = p.y - ctx->verse_drag_start_y;
|
||||||
|
ctx->verse_dragging = false;
|
||||||
|
if (abs(dx) >= 40 && abs(dx) > abs(dy)) {
|
||||||
|
ctx->verse_swipe_consumed = true;
|
||||||
|
manually_advance_verse(ctx, dx < 0 ? 1 : -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void on_verse_tap(lv_event_t* e) {
|
static void on_verse_tap(lv_event_t* e) {
|
||||||
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
||||||
|
if (ctx->verse_swipe_consumed) {
|
||||||
|
ctx->verse_swipe_consumed = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
toggle_ui(ctx);
|
toggle_ui(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1102,11 +1333,11 @@ static void set_chrome_visible(AppCtx* ctx, bool visible) {
|
|||||||
|
|
||||||
static void on_prev_click(lv_event_t* e) {
|
static void on_prev_click(lv_event_t* e) {
|
||||||
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
||||||
advance_verse(ctx, -1);
|
manually_advance_verse(ctx, -1);
|
||||||
}
|
}
|
||||||
static void on_next_click(lv_event_t* e) {
|
static void on_next_click(lv_event_t* e) {
|
||||||
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
||||||
advance_verse(ctx, 1);
|
manually_advance_verse(ctx, 1);
|
||||||
}
|
}
|
||||||
static void on_play_pause_click(lv_event_t* e) {
|
static void on_play_pause_click(lv_event_t* e) {
|
||||||
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
||||||
@@ -1119,6 +1350,17 @@ static void on_play_pause_click(lv_event_t* e) {
|
|||||||
static void on_highlight_click(lv_event_t* e) {
|
static void on_highlight_click(lv_event_t* e) {
|
||||||
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
||||||
toggle_favorite(ctx);
|
toggle_favorite(ctx);
|
||||||
|
if (ctx->favorite_navigation_active && !ctx->is_highlighted) {
|
||||||
|
if (ctx->favorite_count <= 0) {
|
||||||
|
ctx->favorite_navigation_active = false;
|
||||||
|
} else {
|
||||||
|
if (ctx->favorite_dial_idx >= ctx->favorite_count) {
|
||||||
|
ctx->favorite_dial_idx = ctx->favorite_count - 1;
|
||||||
|
}
|
||||||
|
ctx->cur_global = ctx->favorite_indices[ctx->favorite_dial_idx];
|
||||||
|
if (ctx->auto_timer) lv_timer_reset(ctx->auto_timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
// refresh highlight visuals without reloading
|
// refresh highlight visuals without reloading
|
||||||
show_current_verse(ctx);
|
show_current_verse(ctx);
|
||||||
}
|
}
|
||||||
@@ -1197,6 +1439,9 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
|
|||||||
lv_obj_set_style_pad_all(g_ctx.verse_wrapper, 0, 0);
|
lv_obj_set_style_pad_all(g_ctx.verse_wrapper, 0, 0);
|
||||||
lv_obj_remove_flag(g_ctx.verse_wrapper, LV_OBJ_FLAG_SCROLLABLE);
|
lv_obj_remove_flag(g_ctx.verse_wrapper, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
lv_obj_add_flag(g_ctx.verse_wrapper, LV_OBJ_FLAG_CLICKABLE);
|
lv_obj_add_flag(g_ctx.verse_wrapper, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
lv_obj_add_event_cb(g_ctx.verse_wrapper, on_verse_pointer_event, LV_EVENT_PRESSED, &g_ctx);
|
||||||
|
lv_obj_add_event_cb(g_ctx.verse_wrapper, on_verse_pointer_event, LV_EVENT_RELEASED, &g_ctx);
|
||||||
|
lv_obj_add_event_cb(g_ctx.verse_wrapper, on_verse_pointer_event, LV_EVENT_PRESS_LOST, &g_ctx);
|
||||||
lv_obj_add_event_cb(g_ctx.verse_wrapper, on_verse_tap, LV_EVENT_CLICKED, &g_ctx);
|
lv_obj_add_event_cb(g_ctx.verse_wrapper, on_verse_tap, LV_EVENT_CLICKED, &g_ctx);
|
||||||
|
|
||||||
// Center reading column with generous breathing room - avoid top chrome crop
|
// Center reading column with generous breathing room - avoid top chrome crop
|
||||||
@@ -1212,6 +1457,9 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
|
|||||||
lv_obj_set_style_pad_all(center_col, 0, 0);
|
lv_obj_set_style_pad_all(center_col, 0, 0);
|
||||||
lv_obj_remove_flag(center_col, LV_OBJ_FLAG_SCROLLABLE);
|
lv_obj_remove_flag(center_col, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
lv_obj_add_flag(center_col, LV_OBJ_FLAG_CLICKABLE);
|
lv_obj_add_flag(center_col, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
lv_obj_add_event_cb(center_col, on_verse_pointer_event, LV_EVENT_PRESSED, &g_ctx);
|
||||||
|
lv_obj_add_event_cb(center_col, on_verse_pointer_event, LV_EVENT_RELEASED, &g_ctx);
|
||||||
|
lv_obj_add_event_cb(center_col, on_verse_pointer_event, LV_EVENT_PRESS_LOST, &g_ctx);
|
||||||
lv_obj_add_event_cb(center_col, on_verse_tap, LV_EVENT_CLICKED, &g_ctx);
|
lv_obj_add_event_cb(center_col, on_verse_tap, LV_EVENT_CLICKED, &g_ctx);
|
||||||
|
|
||||||
g_ctx.lbl_verse = lv_label_create(center_col);
|
g_ctx.lbl_verse = lv_label_create(center_col);
|
||||||
@@ -1323,7 +1571,7 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
|
|||||||
lv_obj_set_style_radius(g_ctx.btn_highlight, 10, 0);
|
lv_obj_set_style_radius(g_ctx.btn_highlight, 10, 0);
|
||||||
lv_obj_set_style_bg_color(g_ctx.btn_highlight, lv_color_hex(0x252538), 0);
|
lv_obj_set_style_bg_color(g_ctx.btn_highlight, lv_color_hex(0x252538), 0);
|
||||||
lv_obj_set_style_text_color(g_ctx.btn_highlight, lv_color_hex(0x8C8CB0), 0);
|
lv_obj_set_style_text_color(g_ctx.btn_highlight, lv_color_hex(0x8C8CB0), 0);
|
||||||
lbl = lv_label_create(g_ctx.btn_highlight); lv_label_set_text(lbl, LV_SYMBOL_DUMMY); lv_obj_center(lbl);
|
lbl = lv_label_create(g_ctx.btn_highlight); lv_label_set_text(lbl, LV_SYMBOL_PLUS); lv_obj_center(lbl);
|
||||||
lv_obj_add_event_cb(g_ctx.btn_highlight, on_highlight_click, LV_EVENT_CLICKED, &g_ctx);
|
lv_obj_add_event_cb(g_ctx.btn_highlight, on_highlight_click, LV_EVENT_CLICKED, &g_ctx);
|
||||||
|
|
||||||
set_chrome_visible(&g_ctx, true); // start with menu showing per user request
|
set_chrome_visible(&g_ctx, true); // start with menu showing per user request
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
[manifest]
|
manifest.version=0.2
|
||||||
version=0.1
|
target.sdk=0.8.0-dev
|
||||||
[target]
|
target.platforms=esp32s3,esp32p4
|
||||||
sdk=0.8.0-dev
|
app.id=one.tactility.bibleverse
|
||||||
platforms=esp32s3,esp32p4
|
app.version.name=0.1.0
|
||||||
[app]
|
app.version.code=2
|
||||||
id=one.tactility.bibleverse
|
app.name=Bible Verse
|
||||||
versionName=0.1.0
|
app.description=Single verse at a time, advances each minute. Tap to show controls.
|
||||||
versionCode=1
|
|
||||||
name=Bible Verse
|
|
||||||
description=Single verse at a time, advances each minute. Tap to show controls.
|
|
||||||
|
|||||||
+154
-179
@@ -4,7 +4,7 @@
|
|||||||
#include <tt_app_alertdialog.h>
|
#include <tt_app_alertdialog.h>
|
||||||
|
|
||||||
#include <tactility/device.h>
|
#include <tactility/device.h>
|
||||||
#include <tactility/drivers/i2s_controller.h>
|
#include <tactility/drivers/audio_stream.h>
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -45,14 +45,15 @@ typedef struct {
|
|||||||
} BookMetadata;
|
} BookMetadata;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
struct Device* i2s_dev;
|
struct Device* stream_dev;
|
||||||
|
AudioStreamHandle stream_handle;
|
||||||
PlaybackState state;
|
PlaybackState state;
|
||||||
int volume;
|
int volume;
|
||||||
|
|
||||||
// Book picker list data
|
// Book picker list data
|
||||||
BookMetadata books[MAX_BOOKS];
|
BookMetadata books[MAX_BOOKS];
|
||||||
int book_count;
|
int book_count;
|
||||||
|
|
||||||
// Currently loaded book details
|
// Currently loaded book details
|
||||||
char current_book_slug[MAX_PATH];
|
char current_book_slug[MAX_PATH];
|
||||||
cJSON* manifest_root;
|
cJSON* manifest_root;
|
||||||
@@ -60,12 +61,12 @@ typedef struct {
|
|||||||
int page_count;
|
int page_count;
|
||||||
int current_page;
|
int current_page;
|
||||||
int last_pct;
|
int last_pct;
|
||||||
|
|
||||||
// UI elements
|
// UI elements
|
||||||
AppHandle app;
|
AppHandle app;
|
||||||
lv_obj_t* picker_wrapper;
|
lv_obj_t* picker_wrapper;
|
||||||
lv_obj_t* lst_books;
|
lv_obj_t* lst_books;
|
||||||
|
|
||||||
lv_obj_t* player_wrapper;
|
lv_obj_t* player_wrapper;
|
||||||
lv_obj_t* header_bar;
|
lv_obj_t* header_bar;
|
||||||
lv_obj_t* ctrl_bar;
|
lv_obj_t* ctrl_bar;
|
||||||
@@ -76,29 +77,29 @@ typedef struct {
|
|||||||
lv_obj_t* btn_play_pause;
|
lv_obj_t* btn_play_pause;
|
||||||
lv_obj_t* btn_prev;
|
lv_obj_t* btn_prev;
|
||||||
lv_obj_t* btn_next;
|
lv_obj_t* btn_next;
|
||||||
|
|
||||||
// Audio State
|
// Audio State
|
||||||
char current_audio_path[512];
|
char current_audio_path[512];
|
||||||
uint8_t* audio_buf; // Shared MP3 input and WAV buffer
|
uint8_t* audio_buf; // Shared MP3 input and WAV buffer
|
||||||
mp3d_sample_t* pcm_buf; // MP3 decoded pcm buffer
|
mp3d_sample_t* pcm_buf; // MP3 decoded pcm buffer
|
||||||
|
|
||||||
TaskHandle_t playback_task_handle;
|
TaskHandle_t playback_task_handle;
|
||||||
} AppCtx;
|
} AppCtx;
|
||||||
|
|
||||||
typedef struct __attribute__((packed)) {
|
typedef struct __attribute__((packed)) {
|
||||||
char riff[4]; // "RIFF"
|
char riff[4];
|
||||||
uint32_t overall_size; // file size - 8
|
uint32_t overall_size;
|
||||||
char wave[4]; // "WAVE"
|
char wave[4];
|
||||||
char fmt_chunk_marker[4]; // "fmt "
|
char fmt_chunk_marker[4];
|
||||||
uint32_t length_of_fmt; // 16 for PCM
|
uint32_t length_of_fmt;
|
||||||
uint16_t format_type; // 1 for PCM
|
uint16_t format_type;
|
||||||
uint16_t channels; // 1 for mono
|
uint16_t channels;
|
||||||
uint32_t sample_rate; // 16000
|
uint32_t sample_rate;
|
||||||
uint32_t byterate; // sample_rate * channels * (bits_per_sample / 8)
|
uint32_t byterate;
|
||||||
uint16_t block_align; // channels * (bits_per_sample / 8)
|
uint16_t block_align;
|
||||||
uint16_t bits_per_sample; // 16
|
uint16_t bits_per_sample;
|
||||||
char data_chunk_header[4]; // "data"
|
char data_chunk_header[4];
|
||||||
uint32_t data_size; // data size in bytes
|
uint32_t data_size;
|
||||||
} WavHeader;
|
} WavHeader;
|
||||||
|
|
||||||
static AppCtx g_ctx;
|
static AppCtx g_ctx;
|
||||||
@@ -113,19 +114,60 @@ static void play_mp3(AppCtx* ctx);
|
|||||||
static void play_wav(AppCtx* ctx);
|
static void play_wav(AppCtx* ctx);
|
||||||
static void scan_books(AppCtx* ctx);
|
static void scan_books(AppCtx* ctx);
|
||||||
|
|
||||||
|
/* ─── Audio-stream helpers ─── */
|
||||||
|
static bool find_audio_stream_device(AppCtx* ctx) {
|
||||||
|
// Prefer device_find_first_by_type but keep compatibility with name lookup
|
||||||
|
struct Device* dev = device_find_by_name("audio-stream");
|
||||||
|
if (dev) {
|
||||||
|
ctx->stream_dev = dev;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
dev = device_find_first_by_type(&AUDIO_STREAM_TYPE);
|
||||||
|
if (dev) {
|
||||||
|
ctx->stream_dev = dev;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void close_stream_if_open(AppCtx* ctx) {
|
||||||
|
if (ctx->stream_handle) {
|
||||||
|
audio_stream_close(ctx->stream_handle);
|
||||||
|
ctx->stream_handle = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool open_output_stream(AppCtx* ctx, uint32_t sample_rate, uint8_t channels, uint8_t bits) {
|
||||||
|
close_stream_if_open(ctx);
|
||||||
|
if (!ctx->stream_dev) return false;
|
||||||
|
struct AudioStreamConfig cfg = {
|
||||||
|
.sample_rate = sample_rate,
|
||||||
|
.bits_per_sample = bits,
|
||||||
|
.channels = channels
|
||||||
|
};
|
||||||
|
error_t err = audio_stream_open_output(ctx->stream_dev, &cfg, &ctx->stream_handle);
|
||||||
|
if (err != ERROR_NONE) {
|
||||||
|
ESP_LOGE(TAG, "audio_stream_open_output failed: %d (rate=%u ch=%u)", err, (unsigned)sample_rate, channels);
|
||||||
|
ctx->stream_handle = NULL;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "audio_stream output opened: %u Hz %u ch %u-bit", (unsigned)sample_rate, channels, bits);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── UI Helper to update status labels & button states ─── */
|
/* ─── UI Helper to update status labels & button states ─── */
|
||||||
static void update_ui(AppCtx* ctx) {
|
static void update_ui(AppCtx* ctx) {
|
||||||
if (!ctx->btn_play_pause) return;
|
if (!ctx->btn_play_pause) return;
|
||||||
|
|
||||||
lv_obj_t* lbl_play = lv_obj_get_child(ctx->btn_play_pause, 0);
|
lv_obj_t* lbl_play = lv_obj_get_child(ctx->btn_play_pause, 0);
|
||||||
|
|
||||||
// Prev button state
|
// Prev button state
|
||||||
if (ctx->current_page <= 0) {
|
if (ctx->current_page <= 0) {
|
||||||
lv_obj_add_state(ctx->btn_prev, LV_STATE_DISABLED);
|
lv_obj_add_state(ctx->btn_prev, LV_STATE_DISABLED);
|
||||||
} else {
|
} else {
|
||||||
lv_obj_clear_state(ctx->btn_prev, LV_STATE_DISABLED);
|
lv_obj_clear_state(ctx->btn_prev, LV_STATE_DISABLED);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next button state
|
// Next button state
|
||||||
if (ctx->current_page >= ctx->page_count - 1) {
|
if (ctx->current_page >= ctx->page_count - 1) {
|
||||||
lv_obj_add_state(ctx->btn_next, LV_STATE_DISABLED);
|
lv_obj_add_state(ctx->btn_next, LV_STATE_DISABLED);
|
||||||
@@ -162,13 +204,13 @@ static char* read_full_file(const char* filepath) {
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
fseek(file, 0, SEEK_SET);
|
fseek(file, 0, SEEK_SET);
|
||||||
|
|
||||||
char* buf = malloc(size + 1);
|
char* buf = malloc(size + 1);
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
fclose(file);
|
fclose(file);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t read_bytes = fread(buf, 1, size, file);
|
size_t read_bytes = fread(buf, 1, size, file);
|
||||||
buf[read_bytes] = '\0';
|
buf[read_bytes] = '\0';
|
||||||
fclose(file);
|
fclose(file);
|
||||||
@@ -185,7 +227,6 @@ static void on_auto_advance_timer(lv_timer_t* timer) {
|
|||||||
|
|
||||||
static void handle_audio_finished(AppCtx* ctx) {
|
static void handle_audio_finished(AppCtx* ctx) {
|
||||||
if (ctx->current_page + 1 < ctx->page_count) {
|
if (ctx->current_page + 1 < ctx->page_count) {
|
||||||
// Create a one-shot timer with repeat count 1 to run on the GUI thread
|
|
||||||
lv_timer_t* timer = lv_timer_create(on_auto_advance_timer, 0, ctx);
|
lv_timer_t* timer = lv_timer_create(on_auto_advance_timer, 0, ctx);
|
||||||
lv_timer_set_repeat_count(timer, 1);
|
lv_timer_set_repeat_count(timer, 1);
|
||||||
} else {
|
} else {
|
||||||
@@ -208,7 +249,6 @@ static void wait_for_playback_task_to_exit(AppCtx* ctx) {
|
|||||||
|
|
||||||
/* ─── Load Page Content (Image, Caption, Audio path) ─── */
|
/* ─── Load Page Content (Image, Caption, Audio path) ─── */
|
||||||
static void load_page(AppCtx* ctx, int page_index, bool start_audio) {
|
static void load_page(AppCtx* ctx, int page_index, bool start_audio) {
|
||||||
// 1. Stop current audio if running and wait for thread to finish
|
|
||||||
wait_for_playback_task_to_exit(ctx);
|
wait_for_playback_task_to_exit(ctx);
|
||||||
|
|
||||||
if (page_index < 0 || page_index >= ctx->page_count) return;
|
if (page_index < 0 || page_index >= ctx->page_count) return;
|
||||||
@@ -225,7 +265,7 @@ static void load_page(AppCtx* ctx, int page_index, bool start_audio) {
|
|||||||
if (img_item && img_item->valuestring) {
|
if (img_item && img_item->valuestring) {
|
||||||
char img_path[512];
|
char img_path[512];
|
||||||
snprintf(img_path, sizeof(img_path), "/sdcard/books/%s/%s", ctx->current_book_slug, img_item->valuestring);
|
snprintf(img_path, sizeof(img_path), "/sdcard/books/%s/%s", ctx->current_book_slug, img_item->valuestring);
|
||||||
|
|
||||||
FILE* img_file = fopen(img_path, "rb");
|
FILE* img_file = fopen(img_path, "rb");
|
||||||
if (img_file) {
|
if (img_file) {
|
||||||
fclose(img_file);
|
fclose(img_file);
|
||||||
@@ -238,15 +278,13 @@ static void load_page(AppCtx* ctx, int page_index, bool start_audio) {
|
|||||||
lv_image_set_src(ctx->img_page, lv_img_path);
|
lv_image_set_src(ctx->img_page, lv_img_path);
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGW(TAG, "Image file not found: %s", img_path);
|
ESP_LOGW(TAG, "Image file not found: %s", img_path);
|
||||||
lv_image_set_src(ctx->img_page, LV_SYMBOL_IMAGE); // Show default fallback icon
|
lv_image_set_src(ctx->img_page, LV_SYMBOL_IMAGE);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
lv_image_set_src(ctx->img_page, LV_SYMBOL_IMAGE);
|
lv_image_set_src(ctx->img_page, LV_SYMBOL_IMAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update Page index indicator
|
||||||
|
|
||||||
// Update Page index indicator (e.g. "1 / 12")
|
|
||||||
char ind_buf[32];
|
char ind_buf[32];
|
||||||
snprintf(ind_buf, sizeof(ind_buf), "%d / %d", page_index + 1, ctx->page_count);
|
snprintf(ind_buf, sizeof(ind_buf), "%d / %d", page_index + 1, ctx->page_count);
|
||||||
lv_label_set_text(ctx->lbl_indicator, ind_buf);
|
lv_label_set_text(ctx->lbl_indicator, ind_buf);
|
||||||
@@ -258,18 +296,16 @@ static void load_page(AppCtx* ctx, int page_index, bool start_audio) {
|
|||||||
ctx->current_audio_path[0] = '\0';
|
ctx->current_audio_path[0] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update UI states (like Prev/Next buttons)
|
|
||||||
update_ui(ctx);
|
update_ui(ctx);
|
||||||
|
|
||||||
// Start playback if requested and valid
|
// Start playback if requested and valid
|
||||||
if (start_audio && ctx->current_audio_path[0] != '\0') {
|
if (start_audio && ctx->current_audio_path[0] != '\0') {
|
||||||
// Verify audio file exists
|
|
||||||
FILE* audio_file = fopen(ctx->current_audio_path, "rb");
|
FILE* audio_file = fopen(ctx->current_audio_path, "rb");
|
||||||
if (audio_file) {
|
if (audio_file) {
|
||||||
fclose(audio_file);
|
fclose(audio_file);
|
||||||
ctx->state = STATE_PLAYING;
|
ctx->state = STATE_PLAYING;
|
||||||
update_ui(ctx);
|
update_ui(ctx);
|
||||||
xTaskCreate(audio_playback_task, "audio_play", 4096, ctx, 5, &ctx->playback_task_handle);
|
xTaskCreate(audio_playback_task, "audio_play", 8192, ctx, 5, &ctx->playback_task_handle);
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGE(TAG, "Audio file not found: %s", ctx->current_audio_path);
|
ESP_LOGE(TAG, "Audio file not found: %s", ctx->current_audio_path);
|
||||||
const char* buttons[] = {"OK"};
|
const char* buttons[] = {"OK"};
|
||||||
@@ -300,7 +336,7 @@ static void return_to_picker(AppCtx* ctx) {
|
|||||||
/* ─── Task Selection (MP3 or WAV) ─── */
|
/* ─── Task Selection (MP3 or WAV) ─── */
|
||||||
static void audio_playback_task(void* arg) {
|
static void audio_playback_task(void* arg) {
|
||||||
AppCtx* ctx = (AppCtx*)arg;
|
AppCtx* ctx = (AppCtx*)arg;
|
||||||
|
|
||||||
size_t len = strlen(ctx->current_audio_path);
|
size_t len = strlen(ctx->current_audio_path);
|
||||||
bool is_wav = false;
|
bool is_wav = false;
|
||||||
if (len > 4 && strcasecmp(ctx->current_audio_path + len - 4, ".wav") == 0) {
|
if (len > 4 && strcasecmp(ctx->current_audio_path + len - 4, ".wav") == 0) {
|
||||||
@@ -314,9 +350,8 @@ static void audio_playback_task(void* arg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── MP3 Decoder Routine ─── */
|
/* ─── MP3 Decoder Routine (audio-stream) ─── */
|
||||||
static void play_mp3(AppCtx* ctx) {
|
static void play_mp3(AppCtx* ctx) {
|
||||||
// Let the GUI thread load the image from SD first to avoid hardware SD card bus contention
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(400));
|
vTaskDelay(pdMS_TO_TICKS(400));
|
||||||
|
|
||||||
FILE* file = fopen(ctx->current_audio_path, "rb");
|
FILE* file = fopen(ctx->current_audio_path, "rb");
|
||||||
@@ -354,14 +389,12 @@ static void play_mp3(AppCtx* ctx) {
|
|||||||
int sample_rate = 0;
|
int sample_rate = 0;
|
||||||
int channels = 0;
|
int channels = 0;
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Starting MP3 playback: %s (%d bytes)", ctx->current_audio_path, file_size);
|
ESP_LOGI(TAG, "Starting MP3 playback via audio-stream: %s (%d bytes)", ctx->current_audio_path, file_size);
|
||||||
|
|
||||||
while (ctx->state != STATE_IDLE) {
|
while (ctx->state != STATE_IDLE) {
|
||||||
if (ctx->state == STATE_PAUSED) {
|
if (ctx->state == STATE_PAUSED) {
|
||||||
if (sample_rate != 0) {
|
if (ctx->stream_handle) {
|
||||||
device_lock(ctx->i2s_dev);
|
close_stream_if_open(ctx);
|
||||||
i2s_controller_reset(ctx->i2s_dev);
|
|
||||||
device_unlock(ctx->i2s_dev);
|
|
||||||
sample_rate = 0;
|
sample_rate = 0;
|
||||||
channels = 0;
|
channels = 0;
|
||||||
}
|
}
|
||||||
@@ -390,7 +423,6 @@ static void play_mp3(AppCtx* ctx) {
|
|||||||
|
|
||||||
if (info.frame_bytes <= 0) {
|
if (info.frame_bytes <= 0) {
|
||||||
if (eof) break;
|
if (eof) break;
|
||||||
// Resync
|
|
||||||
memmove(ctx->audio_buf, ctx->audio_buf + 1, --buffered_bytes);
|
memmove(ctx->audio_buf, ctx->audio_buf + 1, --buffered_bytes);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -400,22 +432,10 @@ static void play_mp3(AppCtx* ctx) {
|
|||||||
memmove(ctx->audio_buf, ctx->audio_buf + consumed, buffered_bytes);
|
memmove(ctx->audio_buf, ctx->audio_buf + consumed, buffered_bytes);
|
||||||
|
|
||||||
if (samples > 0) {
|
if (samples > 0) {
|
||||||
// Configure I2S
|
// Open/reopen stream on format change
|
||||||
if (sample_rate != info.hz || channels != info.channels) {
|
if (sample_rate != info.hz || channels != info.channels) {
|
||||||
struct I2sConfig config = {
|
if (!open_output_stream(ctx, (uint32_t)info.hz, (uint8_t)info.channels, 16)) {
|
||||||
.communication_format = I2S_FORMAT_STAND_I2S,
|
ESP_LOGE(TAG, "Failed to open audio stream for MP3: %d Hz %d ch", info.hz, info.channels);
|
||||||
.sample_rate = (uint32_t)info.hz,
|
|
||||||
.bits_per_sample = 16,
|
|
||||||
.channel_left = 0,
|
|
||||||
.channel_right = (info.channels == 2) ? 1 : I2S_CHANNEL_NONE
|
|
||||||
};
|
|
||||||
|
|
||||||
device_lock(ctx->i2s_dev);
|
|
||||||
error_t err = i2s_controller_set_config(ctx->i2s_dev, &config);
|
|
||||||
device_unlock(ctx->i2s_dev);
|
|
||||||
|
|
||||||
if (err != ERROR_NONE) {
|
|
||||||
ESP_LOGE(TAG, "Failed to configure I2S: %d", err);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
sample_rate = info.hz;
|
sample_rate = info.hz;
|
||||||
@@ -431,15 +451,15 @@ static void play_mp3(AppCtx* ctx) {
|
|||||||
samples_ptr[i] = (int16_t)scaled;
|
samples_ptr[i] = (int16_t)scaled;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write PCM to I2S
|
// Write via audio_stream (resampled to native 44100 internally)
|
||||||
size_t offset = 0;
|
size_t offset = 0;
|
||||||
size_t data_size = sample_count * sizeof(int16_t);
|
size_t data_size = sample_count * sizeof(int16_t);
|
||||||
bool write_err = false;
|
bool write_err = false;
|
||||||
while (offset < data_size && ctx->state == STATE_PLAYING) {
|
while (offset < data_size && ctx->state == STATE_PLAYING) {
|
||||||
size_t written = 0;
|
size_t written = 0;
|
||||||
error_t err = i2s_controller_write(ctx->i2s_dev, (uint8_t*)ctx->pcm_buf + offset, data_size - offset, &written, pdMS_TO_TICKS(250));
|
error_t err = audio_stream_write(ctx->stream_handle, (uint8_t*)ctx->pcm_buf + offset, data_size - offset, &written, pdMS_TO_TICKS(1000));
|
||||||
if (err != ERROR_NONE || written == 0) {
|
if (err != ERROR_NONE || written == 0) {
|
||||||
ESP_LOGE(TAG, "I2S write error: %d", err);
|
ESP_LOGE(TAG, "audio_stream_write error: %d", err);
|
||||||
write_err = true;
|
write_err = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -465,12 +485,7 @@ static void play_mp3(AppCtx* ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fclose(file);
|
fclose(file);
|
||||||
|
close_stream_if_open(ctx);
|
||||||
if (ctx->i2s_dev) {
|
|
||||||
device_lock(ctx->i2s_dev);
|
|
||||||
i2s_controller_reset(ctx->i2s_dev);
|
|
||||||
device_unlock(ctx->i2s_dev);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool stopped_externally = (ctx->state == STATE_IDLE);
|
bool stopped_externally = (ctx->state == STATE_IDLE);
|
||||||
|
|
||||||
@@ -489,9 +504,8 @@ static void play_mp3(AppCtx* ctx) {
|
|||||||
vTaskDelete(NULL);
|
vTaskDelete(NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── WAV Decoder Routine ─── */
|
/* ─── WAV Decoder Routine (audio-stream) ─── */
|
||||||
static void play_wav(AppCtx* ctx) {
|
static void play_wav(AppCtx* ctx) {
|
||||||
// Let the GUI thread load the image from SD first to avoid hardware SD card bus contention
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(400));
|
vTaskDelay(pdMS_TO_TICKS(400));
|
||||||
|
|
||||||
FILE* file = fopen(ctx->current_audio_path, "rb");
|
FILE* file = fopen(ctx->current_audio_path, "rb");
|
||||||
@@ -538,49 +552,36 @@ static void play_wav(AppCtx* ctx) {
|
|||||||
fseek(file, sizeof(WavHeader), SEEK_SET);
|
fseek(file, sizeof(WavHeader), SEEK_SET);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct I2sConfig config = {
|
if (!open_output_stream(ctx, header.sample_rate, header.channels, header.bits_per_sample)) {
|
||||||
.communication_format = I2S_FORMAT_STAND_I2S,
|
ESP_LOGE(TAG, "Failed to open audio stream for WAV: %u Hz %u ch", (unsigned)header.sample_rate, header.channels);
|
||||||
.sample_rate = header.sample_rate,
|
|
||||||
.bits_per_sample = header.bits_per_sample,
|
|
||||||
.channel_left = 0,
|
|
||||||
.channel_right = (header.channels == 2) ? 1 : I2S_CHANNEL_NONE
|
|
||||||
};
|
|
||||||
|
|
||||||
device_lock(ctx->i2s_dev);
|
|
||||||
error_t err = i2s_controller_set_config(ctx->i2s_dev, &config);
|
|
||||||
device_unlock(ctx->i2s_dev);
|
|
||||||
|
|
||||||
if (err != ERROR_NONE) {
|
|
||||||
ESP_LOGE(TAG, "Failed to configure WAV I2S: %d", err);
|
|
||||||
fclose(file);
|
fclose(file);
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
ctx->state = STATE_IDLE;
|
||||||
|
update_ui(ctx);
|
||||||
|
tt_lvgl_unlock();
|
||||||
ctx->playback_task_handle = NULL;
|
ctx->playback_task_handle = NULL;
|
||||||
vTaskDelete(NULL);
|
vTaskDelete(NULL);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Starting WAV playback: %s (%u Hz, %u channels)", ctx->current_audio_path, (unsigned int)header.sample_rate, (unsigned int)header.channels);
|
ESP_LOGI(TAG, "Starting WAV via audio-stream: %s (%u Hz, %u ch)", ctx->current_audio_path, (unsigned int)header.sample_rate, (unsigned int)header.channels);
|
||||||
|
|
||||||
size_t total_played = 0;
|
size_t total_played = 0;
|
||||||
bool i2s_configured = true;
|
bool stream_open = true;
|
||||||
|
|
||||||
while (total_played < data_size && ctx->state != STATE_IDLE) {
|
while (total_played < data_size && ctx->state != STATE_IDLE) {
|
||||||
if (ctx->state == STATE_PAUSED) {
|
if (ctx->state == STATE_PAUSED) {
|
||||||
if (i2s_configured) {
|
if (stream_open) {
|
||||||
device_lock(ctx->i2s_dev);
|
close_stream_if_open(ctx);
|
||||||
i2s_controller_reset(ctx->i2s_dev);
|
stream_open = false;
|
||||||
device_unlock(ctx->i2s_dev);
|
|
||||||
i2s_configured = false;
|
|
||||||
}
|
}
|
||||||
vTaskDelay(pdMS_TO_TICKS(50));
|
vTaskDelay(pdMS_TO_TICKS(50));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!i2s_configured) {
|
if (!stream_open) {
|
||||||
device_lock(ctx->i2s_dev);
|
if (!open_output_stream(ctx, header.sample_rate, header.channels, header.bits_per_sample)) break;
|
||||||
err = i2s_controller_set_config(ctx->i2s_dev, &config);
|
stream_open = true;
|
||||||
device_unlock(ctx->i2s_dev);
|
|
||||||
if (err != ERROR_NONE) break;
|
|
||||||
i2s_configured = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t to_read = (data_size - total_played < MP3_INPUT_BUFFER_SIZE) ? (data_size - total_played) : MP3_INPUT_BUFFER_SIZE;
|
size_t to_read = (data_size - total_played < MP3_INPUT_BUFFER_SIZE) ? (data_size - total_played) : MP3_INPUT_BUFFER_SIZE;
|
||||||
@@ -596,14 +597,14 @@ static void play_wav(AppCtx* ctx) {
|
|||||||
samples_ptr[i] = (int16_t)scaled;
|
samples_ptr[i] = (int16_t)scaled;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Play to I2S
|
// Write via audio_stream
|
||||||
size_t offset = 0;
|
size_t offset = 0;
|
||||||
bool write_err = false;
|
bool write_err = false;
|
||||||
while (offset < read_bytes && ctx->state == STATE_PLAYING) {
|
while (offset < read_bytes && ctx->state == STATE_PLAYING) {
|
||||||
size_t written = 0;
|
size_t written = 0;
|
||||||
err = i2s_controller_write(ctx->i2s_dev, ctx->audio_buf + offset, read_bytes - offset, &written, pdMS_TO_TICKS(100));
|
error_t err = audio_stream_write(ctx->stream_handle, ctx->audio_buf + offset, read_bytes - offset, &written, pdMS_TO_TICKS(500));
|
||||||
if (err != ERROR_NONE || written == 0) {
|
if (err != ERROR_NONE || written == 0) {
|
||||||
ESP_LOGE(TAG, "I2S WAV write error: %d", err);
|
ESP_LOGE(TAG, "audio_stream WAV write error: %d", err);
|
||||||
write_err = true;
|
write_err = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -614,7 +615,6 @@ static void play_wav(AppCtx* ctx) {
|
|||||||
|
|
||||||
total_played += read_bytes;
|
total_played += read_bytes;
|
||||||
|
|
||||||
// Update progress bar
|
|
||||||
int pct = (int)((total_played * 100) / data_size);
|
int pct = (int)((total_played * 100) / data_size);
|
||||||
if (pct < 0) pct = 0;
|
if (pct < 0) pct = 0;
|
||||||
if (pct > 100) pct = 100;
|
if (pct > 100) pct = 100;
|
||||||
@@ -629,12 +629,7 @@ static void play_wav(AppCtx* ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fclose(file);
|
fclose(file);
|
||||||
|
close_stream_if_open(ctx);
|
||||||
if (ctx->i2s_dev) {
|
|
||||||
device_lock(ctx->i2s_dev);
|
|
||||||
i2s_controller_reset(ctx->i2s_dev);
|
|
||||||
device_unlock(ctx->i2s_dev);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool stopped_externally = (ctx->state == STATE_IDLE);
|
bool stopped_externally = (ctx->state == STATE_IDLE);
|
||||||
|
|
||||||
@@ -652,29 +647,29 @@ static void play_wav(AppCtx* ctx) {
|
|||||||
static void on_book_selected(lv_event_t* e) {
|
static void on_book_selected(lv_event_t* e) {
|
||||||
int index = (int)(intptr_t)lv_event_get_user_data(e);
|
int index = (int)(intptr_t)lv_event_get_user_data(e);
|
||||||
AppCtx* ctx = &g_ctx;
|
AppCtx* ctx = &g_ctx;
|
||||||
|
|
||||||
BookMetadata* book = &ctx->books[index];
|
BookMetadata* book = &ctx->books[index];
|
||||||
strncpy(ctx->current_book_slug, book->slug, sizeof(ctx->current_book_slug) - 1);
|
strncpy(ctx->current_book_slug, book->slug, sizeof(ctx->current_book_slug) - 1);
|
||||||
|
|
||||||
char manifest_path[512];
|
char manifest_path[512];
|
||||||
snprintf(manifest_path, sizeof(manifest_path), "/sdcard/books/%s/manifest.json", book->slug);
|
snprintf(manifest_path, sizeof(manifest_path), "/sdcard/books/%s/manifest.json", book->slug);
|
||||||
|
|
||||||
char* json_str = read_full_file(manifest_path);
|
char* json_str = read_full_file(manifest_path);
|
||||||
if (!json_str) {
|
if (!json_str) {
|
||||||
const char* buttons[] = {"OK"};
|
const char* buttons[] = {"OK"};
|
||||||
tt_app_alertdialog_start("Read Error", "Failed to open the book manifest.", buttons, 1);
|
tt_app_alertdialog_start("Read Error", "Failed to open the book manifest.", buttons, 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx->manifest_root = cJSON_Parse(json_str);
|
ctx->manifest_root = cJSON_Parse(json_str);
|
||||||
free(json_str);
|
free(json_str);
|
||||||
|
|
||||||
if (!ctx->manifest_root) {
|
if (!ctx->manifest_root) {
|
||||||
const char* buttons[] = {"OK"};
|
const char* buttons[] = {"OK"};
|
||||||
tt_app_alertdialog_start("JSON Error", "The book manifest is not formatted correctly.", buttons, 1);
|
tt_app_alertdialog_start("JSON Error", "The book manifest is not formatted correctly.", buttons, 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx->pages_array = cJSON_GetObjectItem(ctx->manifest_root, "pages");
|
ctx->pages_array = cJSON_GetObjectItem(ctx->manifest_root, "pages");
|
||||||
if (!ctx->pages_array || !cJSON_IsArray(ctx->pages_array)) {
|
if (!ctx->pages_array || !cJSON_IsArray(ctx->pages_array)) {
|
||||||
const char* buttons[] = {"OK"};
|
const char* buttons[] = {"OK"};
|
||||||
@@ -684,7 +679,7 @@ static void on_book_selected(lv_event_t* e) {
|
|||||||
ctx->pages_array = NULL;
|
ctx->pages_array = NULL;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx->page_count = cJSON_GetArraySize(ctx->pages_array);
|
ctx->page_count = cJSON_GetArraySize(ctx->pages_array);
|
||||||
if (ctx->page_count <= 0) {
|
if (ctx->page_count <= 0) {
|
||||||
const char* buttons[] = {"OK"};
|
const char* buttons[] = {"OK"};
|
||||||
@@ -694,17 +689,16 @@ static void on_book_selected(lv_event_t* e) {
|
|||||||
ctx->pages_array = NULL;
|
ctx->pages_array = NULL;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lv_label_set_text(ctx->lbl_book_title, book->title);
|
lv_label_set_text(ctx->lbl_book_title, book->title);
|
||||||
|
|
||||||
// Switch views
|
// Switch views
|
||||||
lv_obj_add_flag(ctx->picker_wrapper, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_add_flag(ctx->picker_wrapper, LV_OBJ_FLAG_HIDDEN);
|
||||||
lv_obj_remove_flag(ctx->player_wrapper, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_remove_flag(ctx->player_wrapper, LV_OBJ_FLAG_HIDDEN);
|
||||||
lv_obj_remove_flag(ctx->header_bar, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_remove_flag(ctx->header_bar, LV_OBJ_FLAG_HIDDEN);
|
||||||
lv_obj_remove_flag(ctx->ctrl_bar, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_remove_flag(ctx->ctrl_bar, LV_OBJ_FLAG_HIDDEN);
|
||||||
lv_obj_remove_flag(ctx->bar_progress, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_remove_flag(ctx->bar_progress, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
|
||||||
// Load first page (no auto-play initially)
|
|
||||||
load_page(ctx, 0, false);
|
load_page(ctx, 0, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -712,57 +706,57 @@ static void on_book_selected(lv_event_t* e) {
|
|||||||
static void scan_books(AppCtx* ctx) {
|
static void scan_books(AppCtx* ctx) {
|
||||||
ctx->book_count = 0;
|
ctx->book_count = 0;
|
||||||
lv_obj_clean(ctx->lst_books);
|
lv_obj_clean(ctx->lst_books);
|
||||||
|
|
||||||
DIR* dir = opendir("/sdcard/books");
|
DIR* dir = opendir("/sdcard/books");
|
||||||
if (!dir) {
|
if (!dir) {
|
||||||
ESP_LOGE(TAG, "Failed to scan books: /sdcard/books folder missing.");
|
ESP_LOGE(TAG, "Failed to scan books: /sdcard/books folder missing.");
|
||||||
lv_list_add_text(ctx->lst_books, "No SD card or books directory found.");
|
lv_list_add_text(ctx->lst_books, "No SD card or books directory found.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct dirent* entry;
|
struct dirent* entry;
|
||||||
while ((entry = readdir(dir)) != NULL && ctx->book_count < MAX_BOOKS) {
|
while ((entry = readdir(dir)) != NULL && ctx->book_count < MAX_BOOKS) {
|
||||||
if (entry->d_name[0] == '.') continue;
|
if (entry->d_name[0] == '.') continue;
|
||||||
|
|
||||||
char manifest_path[512];
|
char manifest_path[512];
|
||||||
snprintf(manifest_path, sizeof(manifest_path), "/sdcard/books/%s/manifest.json", entry->d_name);
|
snprintf(manifest_path, sizeof(manifest_path), "/sdcard/books/%s/manifest.json", entry->d_name);
|
||||||
|
|
||||||
char* json_str = read_full_file(manifest_path);
|
char* json_str = read_full_file(manifest_path);
|
||||||
if (json_str) {
|
if (json_str) {
|
||||||
cJSON* root = cJSON_Parse(json_str);
|
cJSON* root = cJSON_Parse(json_str);
|
||||||
free(json_str);
|
free(json_str);
|
||||||
|
|
||||||
if (root) {
|
if (root) {
|
||||||
cJSON* title_item = cJSON_GetObjectItem(root, "title");
|
cJSON* title_item = cJSON_GetObjectItem(root, "title");
|
||||||
cJSON* author_item = cJSON_GetObjectItem(root, "author");
|
cJSON* author_item = cJSON_GetObjectItem(root, "author");
|
||||||
|
|
||||||
BookMetadata* book = &ctx->books[ctx->book_count];
|
BookMetadata* book = &ctx->books[ctx->book_count];
|
||||||
strncpy(book->slug, entry->d_name, sizeof(book->slug) - 1);
|
strncpy(book->slug, entry->d_name, sizeof(book->slug) - 1);
|
||||||
|
|
||||||
if (title_item && title_item->valuestring) {
|
if (title_item && title_item->valuestring) {
|
||||||
strncpy(book->title, title_item->valuestring, sizeof(book->title) - 1);
|
strncpy(book->title, title_item->valuestring, sizeof(book->title) - 1);
|
||||||
} else {
|
} else {
|
||||||
strncpy(book->title, entry->d_name, sizeof(book->title) - 1);
|
strncpy(book->title, entry->d_name, sizeof(book->title) - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (author_item && author_item->valuestring) {
|
if (author_item && author_item->valuestring) {
|
||||||
strncpy(book->author, author_item->valuestring, sizeof(book->author) - 1);
|
strncpy(book->author, author_item->valuestring, sizeof(book->author) - 1);
|
||||||
} else {
|
} else {
|
||||||
book->author[0] = '\0';
|
book->author[0] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
cJSON_Delete(root);
|
cJSON_Delete(root);
|
||||||
ctx->book_count++;
|
ctx->book_count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
closedir(dir);
|
closedir(dir);
|
||||||
|
|
||||||
if (ctx->book_count == 0) {
|
if (ctx->book_count == 0) {
|
||||||
lv_list_add_text(ctx->lst_books, "No book manifest.json files found.");
|
lv_list_add_text(ctx->lst_books, "No book manifest.json files found.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bubble sort by title
|
// Bubble sort by title
|
||||||
for (int i = 0; i < ctx->book_count - 1; i++) {
|
for (int i = 0; i < ctx->book_count - 1; i++) {
|
||||||
for (int j = 0; j < ctx->book_count - i - 1; j++) {
|
for (int j = 0; j < ctx->book_count - i - 1; j++) {
|
||||||
@@ -773,7 +767,7 @@ static void scan_books(AppCtx* ctx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add items to screen list
|
// Add items to screen list
|
||||||
for (int i = 0; i < ctx->book_count; ++i) {
|
for (int i = 0; i < ctx->book_count; ++i) {
|
||||||
char label_text[512];
|
char label_text[512];
|
||||||
@@ -795,7 +789,7 @@ static void on_play_pause_click(lv_event_t* e) {
|
|||||||
if (ctx->state == STATE_IDLE) {
|
if (ctx->state == STATE_IDLE) {
|
||||||
ctx->state = STATE_PLAYING;
|
ctx->state = STATE_PLAYING;
|
||||||
update_ui(ctx);
|
update_ui(ctx);
|
||||||
xTaskCreate(audio_playback_task, "audio_play", 4096, ctx, 5, &ctx->playback_task_handle);
|
xTaskCreate(audio_playback_task, "audio_play", 8192, ctx, 5, &ctx->playback_task_handle);
|
||||||
} else if (ctx->state == STATE_PLAYING) {
|
} else if (ctx->state == STATE_PLAYING) {
|
||||||
ctx->state = STATE_PAUSED;
|
ctx->state = STATE_PAUSED;
|
||||||
update_ui(ctx);
|
update_ui(ctx);
|
||||||
@@ -824,7 +818,7 @@ static void on_next_click(lv_event_t* e) {
|
|||||||
static void on_image_click(lv_event_t* e) {
|
static void on_image_click(lv_event_t* e) {
|
||||||
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
bool is_hidden = lv_obj_has_flag(ctx->header_bar, LV_OBJ_FLAG_HIDDEN);
|
bool is_hidden = lv_obj_has_flag(ctx->header_bar, LV_OBJ_FLAG_HIDDEN);
|
||||||
if (is_hidden) {
|
if (is_hidden) {
|
||||||
lv_obj_remove_flag(ctx->header_bar, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_remove_flag(ctx->header_bar, LV_OBJ_FLAG_HIDDEN);
|
||||||
@@ -847,7 +841,7 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
|
|||||||
memset(&g_ctx, 0, sizeof(g_ctx));
|
memset(&g_ctx, 0, sizeof(g_ctx));
|
||||||
g_ctx.app = app;
|
g_ctx.app = app;
|
||||||
g_ctx.volume = 80;
|
g_ctx.volume = 80;
|
||||||
|
|
||||||
// Allocate shared audio buffers
|
// Allocate shared audio buffers
|
||||||
#ifdef ESP_PLATFORM
|
#ifdef ESP_PLATFORM
|
||||||
g_ctx.audio_buf = (uint8_t*)heap_caps_malloc(MP3_INPUT_BUFFER_SIZE, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
g_ctx.audio_buf = (uint8_t*)heap_caps_malloc(MP3_INPUT_BUFFER_SIZE, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||||
@@ -856,13 +850,12 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
|
|||||||
g_ctx.audio_buf = (uint8_t*)malloc(MP3_INPUT_BUFFER_SIZE);
|
g_ctx.audio_buf = (uint8_t*)malloc(MP3_INPUT_BUFFER_SIZE);
|
||||||
g_ctx.pcm_buf = (mp3d_sample_t*)malloc(MINIMP3_MAX_SAMPLES_PER_FRAME * sizeof(mp3d_sample_t));
|
g_ctx.pcm_buf = (mp3d_sample_t*)malloc(MINIMP3_MAX_SAMPLES_PER_FRAME * sizeof(mp3d_sample_t));
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Find I2S sound device
|
// Find audio-stream device (provides resampling to native 44100)
|
||||||
g_ctx.i2s_dev = device_find_by_name("i2s0");
|
if (!find_audio_stream_device(&g_ctx)) {
|
||||||
if (!g_ctx.i2s_dev) {
|
ESP_LOGE(TAG, "audio-stream device not found! Tried 'audio-stream' name and AUDIO_STREAM_TYPE");
|
||||||
ESP_LOGE(TAG, "I2S device 'i2s0' not found!");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create dual layouts
|
// Create dual layouts
|
||||||
// 1. Picker Screen wrapper
|
// 1. Picker Screen wrapper
|
||||||
g_ctx.picker_wrapper = lv_obj_create(parent);
|
g_ctx.picker_wrapper = lv_obj_create(parent);
|
||||||
@@ -870,13 +863,11 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
|
|||||||
lv_obj_set_style_border_width(g_ctx.picker_wrapper, 0, 0);
|
lv_obj_set_style_border_width(g_ctx.picker_wrapper, 0, 0);
|
||||||
lv_obj_set_style_pad_all(g_ctx.picker_wrapper, 0, 0);
|
lv_obj_set_style_pad_all(g_ctx.picker_wrapper, 0, 0);
|
||||||
lv_obj_set_style_pad_gap(g_ctx.picker_wrapper, 0, 0);
|
lv_obj_set_style_pad_gap(g_ctx.picker_wrapper, 0, 0);
|
||||||
lv_obj_set_style_bg_color(g_ctx.picker_wrapper, lv_color_hex(0x1E1E2E), 0); // Dark background
|
lv_obj_set_style_bg_color(g_ctx.picker_wrapper, lv_color_hex(0x1E1E2E), 0);
|
||||||
|
|
||||||
// Toolbar in picker
|
|
||||||
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(g_ctx.picker_wrapper, app);
|
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(g_ctx.picker_wrapper, app);
|
||||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||||
|
|
||||||
// Picker list
|
|
||||||
g_ctx.lst_books = lv_list_create(g_ctx.picker_wrapper);
|
g_ctx.lst_books = lv_list_create(g_ctx.picker_wrapper);
|
||||||
lv_obj_set_width(g_ctx.lst_books, LV_PCT(100));
|
lv_obj_set_width(g_ctx.lst_books, LV_PCT(100));
|
||||||
lv_obj_align_to(g_ctx.lst_books, toolbar, LV_ALIGN_OUT_BOTTOM_MID, 0, 0);
|
lv_obj_align_to(g_ctx.lst_books, toolbar, LV_ALIGN_OUT_BOTTOM_MID, 0, 0);
|
||||||
@@ -885,7 +876,7 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
|
|||||||
lv_obj_set_height(g_ctx.lst_books, parent_height - toolbar_height);
|
lv_obj_set_height(g_ctx.lst_books, parent_height - toolbar_height);
|
||||||
lv_obj_set_style_bg_color(g_ctx.lst_books, lv_color_hex(0x1E1E2E), 0);
|
lv_obj_set_style_bg_color(g_ctx.lst_books, lv_color_hex(0x1E1E2E), 0);
|
||||||
lv_obj_set_style_border_color(g_ctx.lst_books, lv_color_hex(0x313244), 0);
|
lv_obj_set_style_border_color(g_ctx.lst_books, lv_color_hex(0x313244), 0);
|
||||||
|
|
||||||
// 2. Player Screen wrapper (hidden on start)
|
// 2. Player Screen wrapper (hidden on start)
|
||||||
g_ctx.player_wrapper = lv_obj_create(parent);
|
g_ctx.player_wrapper = lv_obj_create(parent);
|
||||||
lv_obj_set_size(g_ctx.player_wrapper, LV_PCT(100), LV_PCT(100));
|
lv_obj_set_size(g_ctx.player_wrapper, LV_PCT(100), LV_PCT(100));
|
||||||
@@ -893,10 +884,9 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
|
|||||||
lv_obj_set_style_pad_all(g_ctx.player_wrapper, 0, 0);
|
lv_obj_set_style_pad_all(g_ctx.player_wrapper, 0, 0);
|
||||||
lv_obj_set_style_pad_gap(g_ctx.player_wrapper, 0, 0);
|
lv_obj_set_style_pad_gap(g_ctx.player_wrapper, 0, 0);
|
||||||
lv_obj_set_style_bg_color(g_ctx.player_wrapper, lv_color_hex(0x1E1E2E), 0);
|
lv_obj_set_style_bg_color(g_ctx.player_wrapper, lv_color_hex(0x1E1E2E), 0);
|
||||||
lv_obj_remove_flag(g_ctx.player_wrapper, LV_OBJ_FLAG_SCROLLABLE); // Lock page scrolling
|
lv_obj_remove_flag(g_ctx.player_wrapper, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
lv_obj_add_flag(g_ctx.player_wrapper, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_add_flag(g_ctx.player_wrapper, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
|
||||||
// Page Image (covers the whole screen as background)
|
|
||||||
g_ctx.img_page = lv_image_create(g_ctx.player_wrapper);
|
g_ctx.img_page = lv_image_create(g_ctx.player_wrapper);
|
||||||
lv_obj_align(g_ctx.img_page, LV_ALIGN_CENTER, 0, 0);
|
lv_obj_align(g_ctx.img_page, LV_ALIGN_CENTER, 0, 0);
|
||||||
lv_obj_set_style_bg_opa(g_ctx.img_page, LV_OPA_TRANSP, 0);
|
lv_obj_set_style_bg_opa(g_ctx.img_page, LV_OPA_TRANSP, 0);
|
||||||
@@ -905,20 +895,18 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
|
|||||||
lv_obj_remove_flag(g_ctx.img_page, LV_OBJ_FLAG_SCROLLABLE);
|
lv_obj_remove_flag(g_ctx.img_page, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
lv_obj_add_flag(g_ctx.img_page, LV_OBJ_FLAG_CLICKABLE);
|
lv_obj_add_flag(g_ctx.img_page, LV_OBJ_FLAG_CLICKABLE);
|
||||||
lv_obj_add_event_cb(g_ctx.img_page, on_image_click, LV_EVENT_CLICKED, &g_ctx);
|
lv_obj_add_event_cb(g_ctx.img_page, on_image_click, LV_EVENT_CLICKED, &g_ctx);
|
||||||
|
|
||||||
// Custom Player header (Back button, Book Title, Page Indicator)
|
|
||||||
g_ctx.header_bar = lv_obj_create(g_ctx.player_wrapper);
|
g_ctx.header_bar = lv_obj_create(g_ctx.player_wrapper);
|
||||||
lv_obj_t* header_bar = g_ctx.header_bar;
|
lv_obj_t* header_bar = g_ctx.header_bar;
|
||||||
lv_obj_set_size(header_bar, LV_PCT(100), 36);
|
lv_obj_set_size(header_bar, LV_PCT(100), 36);
|
||||||
lv_obj_align(header_bar, LV_ALIGN_TOP_MID, 0, 0);
|
lv_obj_align(header_bar, LV_ALIGN_TOP_MID, 0, 0);
|
||||||
lv_obj_set_style_radius(header_bar, 0, 0);
|
lv_obj_set_style_radius(header_bar, 0, 0);
|
||||||
lv_obj_set_style_bg_color(header_bar, lv_color_hex(0x11111B), 0);
|
lv_obj_set_style_bg_color(header_bar, lv_color_hex(0x11111B), 0);
|
||||||
lv_obj_set_style_bg_opa(header_bar, LV_OPA_COVER, 0); // Solid header bar
|
lv_obj_set_style_bg_opa(header_bar, LV_OPA_COVER, 0);
|
||||||
lv_obj_set_style_border_color(header_bar, lv_color_hex(0x313244), 0);
|
lv_obj_set_style_border_color(header_bar, lv_color_hex(0x313244), 0);
|
||||||
lv_obj_set_style_border_width(header_bar, 1, LV_PART_MAIN);
|
lv_obj_set_style_border_width(header_bar, 1, LV_PART_MAIN);
|
||||||
lv_obj_remove_flag(header_bar, LV_OBJ_FLAG_SCROLLABLE);
|
lv_obj_remove_flag(header_bar, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
|
||||||
// Back button
|
|
||||||
lv_obj_t* btn_back = lv_button_create(header_bar);
|
lv_obj_t* btn_back = lv_button_create(header_bar);
|
||||||
lv_obj_set_size(btn_back, 44, 26);
|
lv_obj_set_size(btn_back, 44, 26);
|
||||||
lv_obj_align(btn_back, LV_ALIGN_LEFT_MID, 0, 0);
|
lv_obj_align(btn_back, LV_ALIGN_LEFT_MID, 0, 0);
|
||||||
@@ -928,37 +916,33 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
|
|||||||
lv_label_set_text(lbl_back, LV_SYMBOL_LEFT);
|
lv_label_set_text(lbl_back, LV_SYMBOL_LEFT);
|
||||||
lv_obj_center(lbl_back);
|
lv_obj_center(lbl_back);
|
||||||
lv_obj_add_event_cb(btn_back, on_back_click, LV_EVENT_CLICKED, &g_ctx);
|
lv_obj_add_event_cb(btn_back, on_back_click, LV_EVENT_CLICKED, &g_ctx);
|
||||||
|
|
||||||
// Title
|
|
||||||
g_ctx.lbl_book_title = lv_label_create(header_bar);
|
g_ctx.lbl_book_title = lv_label_create(header_bar);
|
||||||
lv_obj_align(g_ctx.lbl_book_title, LV_ALIGN_CENTER, 0, 0);
|
lv_obj_align(g_ctx.lbl_book_title, LV_ALIGN_CENTER, 0, 0);
|
||||||
lv_obj_set_width(g_ctx.lbl_book_title, 180);
|
lv_obj_set_width(g_ctx.lbl_book_title, 180);
|
||||||
lv_obj_set_style_text_align(g_ctx.lbl_book_title, LV_TEXT_ALIGN_CENTER, 0);
|
lv_obj_set_style_text_align(g_ctx.lbl_book_title, LV_TEXT_ALIGN_CENTER, 0);
|
||||||
lv_obj_set_style_text_color(g_ctx.lbl_book_title, lv_color_hex(0xCDD6F4), 0);
|
lv_obj_set_style_text_color(g_ctx.lbl_book_title, lv_color_hex(0xCDD6F4), 0);
|
||||||
lv_label_set_long_mode(g_ctx.lbl_book_title, LV_LABEL_LONG_DOT); // Static text with dots to prevent dynamic redraw overhead
|
lv_label_set_long_mode(g_ctx.lbl_book_title, LV_LABEL_LONG_DOT);
|
||||||
|
|
||||||
// Page Indicator
|
|
||||||
g_ctx.lbl_indicator = lv_label_create(header_bar);
|
g_ctx.lbl_indicator = lv_label_create(header_bar);
|
||||||
lv_obj_align(g_ctx.lbl_indicator, LV_ALIGN_RIGHT_MID, 0, 0);
|
lv_obj_align(g_ctx.lbl_indicator, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||||
lv_obj_set_style_text_color(g_ctx.lbl_indicator, lv_color_hex(0xA6ADC8), 0);
|
lv_obj_set_style_text_color(g_ctx.lbl_indicator, lv_color_hex(0xA6ADC8), 0);
|
||||||
lv_label_set_text(g_ctx.lbl_indicator, "1 / 1");
|
lv_label_set_text(g_ctx.lbl_indicator, "1 / 1");
|
||||||
|
|
||||||
// Bottom Controls container (overlaying background image)
|
|
||||||
g_ctx.ctrl_bar = lv_obj_create(g_ctx.player_wrapper);
|
g_ctx.ctrl_bar = lv_obj_create(g_ctx.player_wrapper);
|
||||||
lv_obj_t* ctrl_bar = g_ctx.ctrl_bar;
|
lv_obj_t* ctrl_bar = g_ctx.ctrl_bar;
|
||||||
lv_obj_set_size(ctrl_bar, LV_PCT(100), 40);
|
lv_obj_set_size(ctrl_bar, LV_PCT(100), 40);
|
||||||
lv_obj_align(ctrl_bar, LV_ALIGN_BOTTOM_MID, 0, 0);
|
lv_obj_align(ctrl_bar, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||||
lv_obj_set_style_radius(ctrl_bar, 0, 0);
|
lv_obj_set_style_radius(ctrl_bar, 0, 0);
|
||||||
lv_obj_set_style_bg_color(ctrl_bar, lv_color_hex(0x11111B), 0);
|
lv_obj_set_style_bg_color(ctrl_bar, lv_color_hex(0x11111B), 0);
|
||||||
lv_obj_set_style_bg_opa(ctrl_bar, LV_OPA_COVER, 0); // Solid control bar
|
lv_obj_set_style_bg_opa(ctrl_bar, LV_OPA_COVER, 0);
|
||||||
lv_obj_set_style_border_width(ctrl_bar, 0, 0);
|
lv_obj_set_style_border_width(ctrl_bar, 0, 0);
|
||||||
lv_obj_set_style_pad_all(ctrl_bar, 0, 0);
|
lv_obj_set_style_pad_all(ctrl_bar, 0, 0);
|
||||||
lv_obj_set_style_pad_gap(ctrl_bar, 0, 0);
|
lv_obj_set_style_pad_gap(ctrl_bar, 0, 0);
|
||||||
lv_obj_set_flex_flow(ctrl_bar, LV_FLEX_FLOW_ROW);
|
lv_obj_set_flex_flow(ctrl_bar, LV_FLEX_FLOW_ROW);
|
||||||
lv_obj_set_flex_align(ctrl_bar, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
lv_obj_set_flex_align(ctrl_bar, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
lv_obj_remove_flag(ctrl_bar, LV_OBJ_FLAG_SCROLLABLE);
|
lv_obj_remove_flag(ctrl_bar, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
|
||||||
// Progress Bar (thin bar running along the top of control bar)
|
|
||||||
g_ctx.bar_progress = lv_bar_create(g_ctx.player_wrapper);
|
g_ctx.bar_progress = lv_bar_create(g_ctx.player_wrapper);
|
||||||
lv_obj_set_size(g_ctx.bar_progress, LV_PCT(100), 4);
|
lv_obj_set_size(g_ctx.bar_progress, LV_PCT(100), 4);
|
||||||
lv_obj_align_to(g_ctx.bar_progress, ctrl_bar, LV_ALIGN_OUT_TOP_MID, 0, 0);
|
lv_obj_align_to(g_ctx.bar_progress, ctrl_bar, LV_ALIGN_OUT_TOP_MID, 0, 0);
|
||||||
@@ -966,8 +950,7 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
|
|||||||
lv_bar_set_value(g_ctx.bar_progress, 0, LV_ANIM_OFF);
|
lv_bar_set_value(g_ctx.bar_progress, 0, LV_ANIM_OFF);
|
||||||
lv_obj_set_style_bg_color(g_ctx.bar_progress, lv_color_hex(0x45475A), LV_PART_MAIN);
|
lv_obj_set_style_bg_color(g_ctx.bar_progress, lv_color_hex(0x45475A), LV_PART_MAIN);
|
||||||
lv_obj_set_style_bg_color(g_ctx.bar_progress, lv_color_hex(0x89B4FA), LV_PART_INDICATOR);
|
lv_obj_set_style_bg_color(g_ctx.bar_progress, lv_color_hex(0x89B4FA), LV_PART_INDICATOR);
|
||||||
|
|
||||||
// Prev Button
|
|
||||||
g_ctx.btn_prev = lv_button_create(ctrl_bar);
|
g_ctx.btn_prev = lv_button_create(ctrl_bar);
|
||||||
lv_obj_set_size(g_ctx.btn_prev, 54, 30);
|
lv_obj_set_size(g_ctx.btn_prev, 54, 30);
|
||||||
lv_obj_set_style_radius(g_ctx.btn_prev, 15, 0);
|
lv_obj_set_style_radius(g_ctx.btn_prev, 15, 0);
|
||||||
@@ -976,8 +959,7 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
|
|||||||
lv_label_set_text(lbl_prev, LV_SYMBOL_PREV);
|
lv_label_set_text(lbl_prev, LV_SYMBOL_PREV);
|
||||||
lv_obj_center(lbl_prev);
|
lv_obj_center(lbl_prev);
|
||||||
lv_obj_add_event_cb(g_ctx.btn_prev, on_prev_click, LV_EVENT_CLICKED, &g_ctx);
|
lv_obj_add_event_cb(g_ctx.btn_prev, on_prev_click, LV_EVENT_CLICKED, &g_ctx);
|
||||||
|
|
||||||
// Play/Pause Button
|
|
||||||
g_ctx.btn_play_pause = lv_button_create(ctrl_bar);
|
g_ctx.btn_play_pause = lv_button_create(ctrl_bar);
|
||||||
lv_obj_set_size(g_ctx.btn_play_pause, 94, 30);
|
lv_obj_set_size(g_ctx.btn_play_pause, 94, 30);
|
||||||
lv_obj_set_style_radius(g_ctx.btn_play_pause, 15, 0);
|
lv_obj_set_style_radius(g_ctx.btn_play_pause, 15, 0);
|
||||||
@@ -987,8 +969,7 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
|
|||||||
lv_label_set_text(lbl_play_btn, LV_SYMBOL_PLAY " Play");
|
lv_label_set_text(lbl_play_btn, LV_SYMBOL_PLAY " Play");
|
||||||
lv_obj_center(lbl_play_btn);
|
lv_obj_center(lbl_play_btn);
|
||||||
lv_obj_add_event_cb(g_ctx.btn_play_pause, on_play_pause_click, LV_EVENT_CLICKED, &g_ctx);
|
lv_obj_add_event_cb(g_ctx.btn_play_pause, on_play_pause_click, LV_EVENT_CLICKED, &g_ctx);
|
||||||
|
|
||||||
// Next Button
|
|
||||||
g_ctx.btn_next = lv_button_create(ctrl_bar);
|
g_ctx.btn_next = lv_button_create(ctrl_bar);
|
||||||
lv_obj_set_size(g_ctx.btn_next, 54, 30);
|
lv_obj_set_size(g_ctx.btn_next, 54, 30);
|
||||||
lv_obj_set_style_radius(g_ctx.btn_next, 15, 0);
|
lv_obj_set_style_radius(g_ctx.btn_next, 15, 0);
|
||||||
@@ -1001,26 +982,20 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
|
|||||||
// Initial load
|
// Initial load
|
||||||
g_ctx.state = STATE_IDLE;
|
g_ctx.state = STATE_IDLE;
|
||||||
update_ui(&g_ctx);
|
update_ui(&g_ctx);
|
||||||
|
|
||||||
// Scan Books
|
|
||||||
scan_books(&g_ctx);
|
scan_books(&g_ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void onHideApp(AppHandle app, void* data) {
|
static void onHideApp(AppHandle app, void* data) {
|
||||||
wait_for_playback_task_to_exit(&g_ctx);
|
wait_for_playback_task_to_exit(&g_ctx);
|
||||||
|
close_stream_if_open(&g_ctx);
|
||||||
if (g_ctx.i2s_dev) {
|
|
||||||
device_lock(g_ctx.i2s_dev);
|
|
||||||
i2s_controller_reset(g_ctx.i2s_dev);
|
|
||||||
device_unlock(g_ctx.i2s_dev);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (g_ctx.manifest_root) {
|
if (g_ctx.manifest_root) {
|
||||||
cJSON_Delete(g_ctx.manifest_root);
|
cJSON_Delete(g_ctx.manifest_root);
|
||||||
g_ctx.manifest_root = NULL;
|
g_ctx.manifest_root = NULL;
|
||||||
g_ctx.pages_array = NULL;
|
g_ctx.pages_array = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef ESP_PLATFORM
|
#ifdef ESP_PLATFORM
|
||||||
if (g_ctx.audio_buf) {
|
if (g_ctx.audio_buf) {
|
||||||
heap_caps_free(g_ctx.audio_buf);
|
heap_caps_free(g_ctx.audio_buf);
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
[manifest]
|
manifest.version=0.2
|
||||||
version=0.1
|
target.sdk=0.8.0-dev
|
||||||
[target]
|
target.platforms=esp32s3
|
||||||
sdk=0.8.0-dev
|
app.id=one.tactility.bookplayer
|
||||||
platforms=esp32s3
|
app.version.name=1.0.0
|
||||||
[app]
|
app.version.code=1
|
||||||
id=one.tactility.bookplayer
|
app.name=Book Player
|
||||||
versionName=1.0.0
|
|
||||||
versionCode=1
|
|
||||||
name=Book Player
|
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
[manifest]
|
manifest.version=0.2
|
||||||
version=0.1
|
target.sdk=0.8.0-dev
|
||||||
[target]
|
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
sdk=0.7.0-dev
|
app.id=one.tactility.brainfuck
|
||||||
platforms=esp32,esp32s3,esp32c6,esp32p4
|
app.version.name=0.5.0
|
||||||
[app]
|
app.version.code=5
|
||||||
id=one.tactility.brainfuck
|
app.name=Brainfuck interpreter
|
||||||
versionName=0.2.0
|
app.description=Brainfuck esoteric language interpreter
|
||||||
versionCode=2
|
|
||||||
name=Brainfuck interpreter
|
|
||||||
description=Brainfuck esoteric language interpreter
|
|
||||||
|
|||||||
@@ -124,7 +124,6 @@ void Breakout::onShow(AppHandle appHandle, lv_obj_t* parent) {
|
|||||||
if (!sfxEngine) {
|
if (!sfxEngine) {
|
||||||
sfxEngine = new SfxEngine();
|
sfxEngine = new SfxEngine();
|
||||||
sfxEngine->start();
|
sfxEngine->start();
|
||||||
sfxEngine->applyVolumePreset(SfxEngine::VolumePreset::Quiet);
|
|
||||||
sfxEngine->setEnabled(soundEnabled);
|
sfxEngine->setEnabled(soundEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
[manifest]
|
manifest.version=0.2
|
||||||
version=0.1
|
target.sdk=0.8.0-dev
|
||||||
[target]
|
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
sdk=0.7.0-dev
|
app.id=one.tactility.breakout
|
||||||
platforms=esp32,esp32s3,esp32c6,esp32p4
|
app.version.name=0.5.0
|
||||||
[app]
|
app.version.code=5
|
||||||
id=one.tactility.breakout
|
app.name=Breakout
|
||||||
versionName=0.2.0
|
app.description=Classic brick-breaking arcade game
|
||||||
versionCode=2
|
|
||||||
name=Breakout
|
|
||||||
description=Classic brick-breaking arcade game
|
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
[manifest]
|
manifest.version=0.2
|
||||||
version=0.1
|
target.sdk=0.8.0-dev
|
||||||
[target]
|
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
sdk=0.7.0-dev
|
app.id=one.tactility.calculator
|
||||||
platforms=esp32,esp32s3,esp32c6,esp32p4
|
app.version.name=0.6.0
|
||||||
[app]
|
app.version.code=6
|
||||||
id=one.tactility.calculator
|
app.name=Calculator
|
||||||
versionName=0.3.0
|
|
||||||
versionCode=3
|
|
||||||
name=Calculator
|
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
[manifest]
|
manifest.version=0.2
|
||||||
version=0.1
|
target.sdk=0.8.0-dev
|
||||||
[target]
|
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
sdk=0.7.0-dev
|
app.id=one.tactility.diceware
|
||||||
platforms=esp32,esp32s3,esp32c6,esp32p4
|
app.version.name=0.6.0
|
||||||
[app]
|
app.version.code=6
|
||||||
id=one.tactility.diceware
|
app.name=Diceware
|
||||||
versionName=0.3.0
|
|
||||||
versionCode=3
|
|
||||||
name=Diceware
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
|
if (DEFINED ENV{TACTILITY_SDK_PATH})
|
||||||
|
set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH})
|
||||||
|
else()
|
||||||
|
set(TACTILITY_SDK_PATH "../../release/TactilitySDK")
|
||||||
|
message(WARNING "TACTILITY_SDK_PATH not set, defaulting to ${TACTILITY_SDK_PATH}")
|
||||||
|
endif()
|
||||||
|
include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake")
|
||||||
|
set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH})
|
||||||
|
project(DiscoveryMountain)
|
||||||
|
tactility_project(DiscoveryMountain)
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
# 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
|
||||||
@@ -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`
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
file(GLOB_RECURSE SOURCE_FILES Source/*.c)
|
||||||
|
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
|
||||||
|
)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#include "app_context.h"
|
||||||
|
|
||||||
|
AppCtx G = {0};
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <tt_lvgl.h>
|
||||||
|
#include <tt_lvgl_toolbar.h>
|
||||||
|
#include <lvgl.h>
|
||||||
|
#include <tactility/lvgl_fonts.h>
|
||||||
|
#include <tactility/device.h>
|
||||||
|
#include <tactility/drivers/audio_stream.h>
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define TAG "DM"
|
||||||
|
#define PB "http://192.168.68.110:8095"
|
||||||
|
#define DM_DIR "/sdcard/dm"
|
||||||
|
#define STATE_PATH "/sdcard/apps/one.tactility.discoverymountain/userdata/state.json"
|
||||||
|
#define CACHE_PATH "/sdcard/apps/one.tactility.discoverymountain/userdata/cache.json"
|
||||||
|
#define MAX_SEASONS 40
|
||||||
|
#define MAX_EPS 250
|
||||||
|
#define MP3_BUF 4096
|
||||||
|
|
||||||
|
typedef struct { int num; } SeasonInfo;
|
||||||
|
typedef struct {
|
||||||
|
char title[64];
|
||||||
|
char slug[96];
|
||||||
|
int season;
|
||||||
|
int ep_num;
|
||||||
|
char audio_url[128];
|
||||||
|
bool seen;
|
||||||
|
} EpInfo;
|
||||||
|
|
||||||
|
typedef struct { char slug[96]; int pos; } SavedPos;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
SeasonInfo seasons[MAX_SEASONS];
|
||||||
|
int season_cnt;
|
||||||
|
EpInfo eps[MAX_EPS];
|
||||||
|
int ep_cnt;
|
||||||
|
int cur_season;
|
||||||
|
int cur_ep_idx;
|
||||||
|
struct Device* stream_dev;
|
||||||
|
AudioStreamHandle stream_handle;
|
||||||
|
bool is_playing;
|
||||||
|
bool is_paused;
|
||||||
|
bool need_stop;
|
||||||
|
TaskHandle_t play_handle;
|
||||||
|
TaskHandle_t fetch_handle;
|
||||||
|
TaskHandle_t dl_handle;
|
||||||
|
int pos_sec;
|
||||||
|
int total_sec;
|
||||||
|
int volume;
|
||||||
|
int last_pct;
|
||||||
|
lv_obj_t *root;
|
||||||
|
lv_obj_t *lbl_title, *lbl_meta, *bar, *lbl_time, *lbl_status, *btn_play;
|
||||||
|
lv_obj_t *lbl_season;
|
||||||
|
lv_obj_t *ep_list_cont;
|
||||||
|
lv_obj_t *btn_seasons;
|
||||||
|
lv_obj_t *btn_dl_missing;
|
||||||
|
lv_obj_t *overlay, *dropdown;
|
||||||
|
lv_obj_t *season_list_cont;
|
||||||
|
lv_timer_t *ui_timer_handle;
|
||||||
|
// Download UI
|
||||||
|
lv_obj_t *dl_overlay;
|
||||||
|
lv_obj_t *dl_bar;
|
||||||
|
lv_obj_t *dl_lbl_pct;
|
||||||
|
lv_obj_t *dl_lbl_detail;
|
||||||
|
lv_obj_t *dl_lbl_title;
|
||||||
|
lv_obj_t *dl_lbl_sub;
|
||||||
|
char cur_title[128];
|
||||||
|
char last_slug[96];
|
||||||
|
bool fetching;
|
||||||
|
bool downloading;
|
||||||
|
volatile int dl_total;
|
||||||
|
int dl_expected;
|
||||||
|
int dl_last_ui_update;
|
||||||
|
volatile bool dl_cancel_req;
|
||||||
|
volatile bool fetch_cancel_req;
|
||||||
|
char dl_ep_title[128];
|
||||||
|
char dl_slug[96];
|
||||||
|
int dl_season;
|
||||||
|
int dl_ep_num;
|
||||||
|
int dl_last_pct;
|
||||||
|
// Per-episode resume
|
||||||
|
SavedPos saved_pos[MAX_EPS];
|
||||||
|
int saved_pos_cnt;
|
||||||
|
int last_save_tick;
|
||||||
|
|
||||||
|
// External downloader integration (SdDownloader)
|
||||||
|
int pending_dl_idx;
|
||||||
|
|
||||||
|
// Full season download queue (sequential single-file to avoid batch crash)
|
||||||
|
bool dl_missing_active;
|
||||||
|
int dl_missing_season;
|
||||||
|
int dl_missing_total;
|
||||||
|
int dl_missing_done;
|
||||||
|
int dl_missing_failed;
|
||||||
|
|
||||||
|
// Refresh button reference
|
||||||
|
lv_obj_t *btn_refresh;
|
||||||
|
} AppCtx;
|
||||||
|
|
||||||
|
void trigger_manual_refresh(void);
|
||||||
|
void dm_preserve_season(int season);
|
||||||
|
int dm_consume_preserved_season(void);
|
||||||
|
|
||||||
|
extern AppCtx G;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#include "download.h"
|
||||||
|
#include "storage.h"
|
||||||
|
#include <tt_lvgl.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "esp_log.h"
|
||||||
|
|
||||||
|
// Simplified: internal download removed, we use external SdDownloader app
|
||||||
|
// Keep overlay functions as stubs for compatibility, but they do minimal
|
||||||
|
|
||||||
|
void hide_dl_overlay(){
|
||||||
|
if(G.dl_overlay){
|
||||||
|
lv_obj_delete(G.dl_overlay);
|
||||||
|
G.dl_overlay=NULL;
|
||||||
|
G.dl_bar=NULL;
|
||||||
|
G.dl_lbl_pct=NULL;
|
||||||
|
G.dl_lbl_detail=NULL;
|
||||||
|
G.dl_lbl_title=NULL;
|
||||||
|
G.dl_lbl_sub=NULL;
|
||||||
|
}
|
||||||
|
G.downloading=false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void show_dl_overlay_locked(const char* ep_title, const char* slug, int season, int ep_num){
|
||||||
|
// No longer used - external downloader has its own UI
|
||||||
|
// Keep as stub that shows simple status
|
||||||
|
(void)ep_title; (void)slug; (void)season; (void)ep_num;
|
||||||
|
G.downloading=true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_dl_overlay_ui(){
|
||||||
|
// No internal download progress anymore
|
||||||
|
}
|
||||||
|
|
||||||
|
bool dl_ep_raw(EpInfo* ep){
|
||||||
|
(void)ep;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool dl_ep(EpInfo* ep){
|
||||||
|
(void)ep;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void dl_task(void* arg){
|
||||||
|
(void)arg;
|
||||||
|
G.downloading=false;
|
||||||
|
G.dl_handle=NULL;
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "app_context.h"
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Raw download to file with progress tracking
|
||||||
|
bool dl_ep_raw(EpInfo* ep);
|
||||||
|
bool dl_ep(EpInfo* ep);
|
||||||
|
|
||||||
|
// Download task
|
||||||
|
void dl_task(void* arg);
|
||||||
|
|
||||||
|
// Download overlay UI
|
||||||
|
void hide_dl_overlay(void);
|
||||||
|
void show_dl_overlay_locked(const char* ep_title, const char* slug, int season, int ep_num);
|
||||||
|
void update_dl_overlay_ui(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,491 @@
|
|||||||
|
#include <tt_app.h>
|
||||||
|
#include <tt_lvgl.h>
|
||||||
|
#include <tt_bundle.h>
|
||||||
|
#include "app_context.h"
|
||||||
|
#include "storage.h"
|
||||||
|
#include "network.h"
|
||||||
|
#include "download.h"
|
||||||
|
#include "player.h"
|
||||||
|
#include "ui.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_heap_caps.h"
|
||||||
|
|
||||||
|
// Preserved browsing season that survives G memset across external app launch/return
|
||||||
|
// This fixes season reset to 1 when returning from player/downloader where file read may race
|
||||||
|
static int s_preserved_season = -1;
|
||||||
|
|
||||||
|
void dm_preserve_season(int season){
|
||||||
|
if(season>0) s_preserved_season = season;
|
||||||
|
}
|
||||||
|
int dm_consume_preserved_season(void){
|
||||||
|
int s = s_preserved_season;
|
||||||
|
s_preserved_season = -1;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void fetch_task_fn(void* arg){
|
||||||
|
(void)arg;
|
||||||
|
G.fetching=true;
|
||||||
|
G.fetch_cancel_req=false;
|
||||||
|
ESP_LOGI(TAG,"fetch task start (manual refresh)");
|
||||||
|
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if(G.lbl_status) lv_label_set_text(G.lbl_status,"Fetching from network...");
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
|
||||||
|
// Preserve user's current season selection across fetch
|
||||||
|
int preserved_season = G.cur_season;
|
||||||
|
char preserved_slug[96]={0};
|
||||||
|
if(G.cur_ep_idx>=0 && G.cur_ep_idx < G.ep_cnt){
|
||||||
|
strncpy(preserved_slug, G.eps[G.cur_ep_idx].slug, sizeof(preserved_slug)-1);
|
||||||
|
} else if(strlen(G.last_slug)>0){
|
||||||
|
strncpy(preserved_slug, G.last_slug, sizeof(preserved_slug)-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool fetch_ok = fetch_data();
|
||||||
|
|
||||||
|
if(G.fetch_cancel_req){
|
||||||
|
ESP_LOGI(TAG,"fetch task canceled");
|
||||||
|
G.fetching=false;
|
||||||
|
G.fetch_handle=NULL;
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(fetch_ok){
|
||||||
|
ESP_LOGI(TAG,"Network fetch ok: %d seasons, %d eps – saving cache", G.season_cnt, G.ep_cnt);
|
||||||
|
save_cache();
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG,"Network fetch failed");
|
||||||
|
// Do not overwrite current data if fetch fails – keep existing cache in memory
|
||||||
|
// If we have no data at all, fetch_data already created fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG,"fetch done seasons=%d eps=%d fetch_ok=%d", G.season_cnt, G.ep_cnt, fetch_ok);
|
||||||
|
|
||||||
|
// Restore preserved season if it still exists
|
||||||
|
if(preserved_season>0){
|
||||||
|
bool season_exists=false;
|
||||||
|
for(int i=0;i<G.season_cnt;i++) if(G.seasons[i].num==preserved_season){ season_exists=true; break; }
|
||||||
|
if(season_exists){
|
||||||
|
G.cur_season = preserved_season;
|
||||||
|
// try to restore slug within preserved season
|
||||||
|
int best=-1;
|
||||||
|
if(strlen(preserved_slug)>0){
|
||||||
|
for(int i=0;i<G.ep_cnt;i++) if(G.eps[i].season==preserved_season && strcmp(G.eps[i].slug,preserved_slug)==0){ best=i; break; }
|
||||||
|
}
|
||||||
|
if(best==-1){
|
||||||
|
for(int i=0;i<G.ep_cnt;i++) if(G.eps[i].season==preserved_season){ best=i; break; }
|
||||||
|
}
|
||||||
|
if(best!=-1) G.cur_ep_idx=best;
|
||||||
|
strncpy(G.last_slug, preserved_slug, sizeof(G.last_slug)-1);
|
||||||
|
} else {
|
||||||
|
// Fallback to preserved slug or first
|
||||||
|
if(strlen(preserved_slug)>0){
|
||||||
|
for(int i=0;i<G.ep_cnt;i++) if(strcmp(G.eps[i].slug,preserved_slug)==0){ G.cur_ep_idx=i; G.cur_season=G.eps[i].season; break; }
|
||||||
|
}
|
||||||
|
if(G.cur_ep_idx<0 && G.ep_cnt>0){
|
||||||
|
G.cur_ep_idx=0;
|
||||||
|
G.cur_season=G.eps[0].season;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(G.cur_ep_idx<0 && G.ep_cnt>0){
|
||||||
|
G.cur_ep_idx=0;
|
||||||
|
G.cur_season=G.eps[0].season;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
G.fetching=false;
|
||||||
|
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if(G.fetch_cancel_req){
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
G.fetch_handle=NULL;
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
|
||||||
|
ui_rebuild_episode_list();
|
||||||
|
|
||||||
|
if(G.ep_cnt>0 && G.cur_ep_idx>=0){
|
||||||
|
EpInfo* ep=&G.eps[G.cur_ep_idx];
|
||||||
|
int saved = get_saved_pos_for_slug(ep->slug);
|
||||||
|
if(saved==0) saved = G.pos_sec;
|
||||||
|
if(saved>0) G.pos_sec=saved;
|
||||||
|
}
|
||||||
|
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if(G.lbl_status){
|
||||||
|
if(G.ep_cnt>0){
|
||||||
|
if(fetch_ok) lv_label_set_text(G.lbl_status, "Refreshed – select episode");
|
||||||
|
else lv_label_set_text(G.lbl_status, "Refresh failed – using cache");
|
||||||
|
} else {
|
||||||
|
lv_label_set_text(G.lbl_status,"No data – check network");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
|
||||||
|
G.fetch_handle=NULL;
|
||||||
|
save_state();
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void trigger_manual_refresh(void){
|
||||||
|
if(G.fetching){
|
||||||
|
ESP_LOGI(TAG,"refresh already in progress");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(G.fetch_handle){
|
||||||
|
ESP_LOGW(TAG,"fetch handle still present, not starting new");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
G.fetch_cancel_req=false;
|
||||||
|
size_t free_internal = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
|
||||||
|
size_t free_psram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
|
||||||
|
ESP_LOGI(TAG,"trigger_manual_refresh heap: internal=%d psram=%d", free_internal, free_psram);
|
||||||
|
BaseType_t res = xTaskCreate(fetch_task_fn,"dm_fetch",12288,NULL,5,&G.fetch_handle);
|
||||||
|
ESP_LOGI(TAG,"xTaskCreate manual refresh res=%d handle=%p", res, G.fetch_handle);
|
||||||
|
if(res != pdPASS){
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if(G.lbl_status) lv_label_set_text(G.lbl_status,"Failed to start refresh");
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
G.fetch_handle=NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void onShow(AppHandle app, void* data, lv_obj_t* parent){
|
||||||
|
(void)app; (void)data; (void)parent;
|
||||||
|
// Check for preserved season from external app launch (survives G memset)
|
||||||
|
int preserved = dm_consume_preserved_season();
|
||||||
|
if(preserved>0){
|
||||||
|
ESP_LOGI(TAG,"onShow preserved season S%02d from previous launch", preserved);
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(&G,0,sizeof(G));
|
||||||
|
G.pos_sec=0;
|
||||||
|
G.total_sec=0;
|
||||||
|
G.cur_season=1;
|
||||||
|
G.cur_ep_idx=-1;
|
||||||
|
G.pending_dl_idx=-1;
|
||||||
|
G.volume=80;
|
||||||
|
G.last_pct=-1;
|
||||||
|
G.dl_last_pct=-1;
|
||||||
|
G.dl_expected=-1;
|
||||||
|
G.fetch_cancel_req=false;
|
||||||
|
G.dl_cancel_req=false;
|
||||||
|
ensure_dir();
|
||||||
|
load_state();
|
||||||
|
|
||||||
|
// If we have a preserved season from launching player/downloader, it overrides file
|
||||||
|
if(preserved>0){
|
||||||
|
G.cur_season = preserved;
|
||||||
|
ESP_LOGI(TAG,"onShow overriding cur_season with preserved S%02d", preserved);
|
||||||
|
}
|
||||||
|
|
||||||
|
build_ui(parent);
|
||||||
|
|
||||||
|
// Instant cache load – no network, fully synchronous for fast startup
|
||||||
|
// NOTE: load_state() already loaded last_season (browsing season) and last_slug (last played)
|
||||||
|
// We want to preserve browsing season, NOT overwrite it with last_slug's season.
|
||||||
|
bool cache_loaded = load_cache();
|
||||||
|
if(cache_loaded){
|
||||||
|
ESP_LOGI(TAG,"Cache loaded instantly: %d seasons, %d eps browsing S%02d last_slug=%s preserved=%d", G.season_cnt, G.ep_cnt, G.cur_season, G.last_slug, preserved);
|
||||||
|
// Validate browsing season still exists, otherwise fallback to first season
|
||||||
|
bool season_exists=false;
|
||||||
|
for(int i=0;i<G.season_cnt;i++) if(G.seasons[i].num==G.cur_season){ season_exists=true; break; }
|
||||||
|
if(!season_exists){
|
||||||
|
// If we had a preserved season but it's not in seasons list, maybe list incomplete – try preserved again
|
||||||
|
if(preserved>0){
|
||||||
|
for(int i=0;i<G.season_cnt;i++) if(G.seasons[i].num==preserved){ season_exists=true; G.cur_season=preserved; break; }
|
||||||
|
}
|
||||||
|
if(!season_exists && G.season_cnt>0){
|
||||||
|
ESP_LOGW(TAG,"Browsing season S%02d not found in cache, fallback to S%02d", G.cur_season, G.seasons[0].num);
|
||||||
|
G.cur_season = G.seasons[0].num;
|
||||||
|
season_exists=true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Resolve cur_ep_idx: prefer last_slug if it is within browsing season, otherwise first ep in browsing season
|
||||||
|
G.cur_ep_idx=-1;
|
||||||
|
if(strlen(G.last_slug)>0 && season_exists){
|
||||||
|
for(int i=0;i<G.ep_cnt;i++) if(G.eps[i].season==G.cur_season && strcmp(G.eps[i].slug,G.last_slug)==0){ G.cur_ep_idx=i; break; }
|
||||||
|
}
|
||||||
|
if(G.cur_ep_idx<0){
|
||||||
|
for(int i=0;i<G.ep_cnt;i++) if(G.eps[i].season==G.cur_season){ G.cur_ep_idx=i; break; }
|
||||||
|
}
|
||||||
|
if(G.cur_ep_idx<0 && G.ep_cnt>0){
|
||||||
|
G.cur_ep_idx=0;
|
||||||
|
G.cur_season=G.eps[0].season;
|
||||||
|
}
|
||||||
|
ui_rebuild_episode_list();
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if(G.lbl_status) lv_label_set_text(G.lbl_status,"Loaded from cache – tap Refresh to update");
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
} else {
|
||||||
|
ESP_LOGI(TAG,"No cache at startup, cur_season S%02d preserved %d", G.cur_season, preserved);
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if(G.lbl_status) lv_label_set_text(G.lbl_status,"No cache – tap Refresh");
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
ui_rebuild_episode_list();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final safeguard: if preserved was set, ensure it is saved back to state file so next open keeps it
|
||||||
|
if(preserved>0){
|
||||||
|
save_state();
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t free_internal = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
|
||||||
|
size_t free_psram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
|
||||||
|
ESP_LOGI(TAG, "onShow heap after cache load: internal=%d psram=%d cache=%d cur_season=%d", free_internal, free_psram, cache_loaded, G.cur_season);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void onResult(AppHandle app, void* data, AppLaunchId launchId, AppResult result, BundleHandle resultData){
|
||||||
|
(void)app; (void)data; (void)launchId;
|
||||||
|
ESP_LOGI(TAG, "onResult: result=%d pending_idx=%d cur_idx=%d", result, G.pending_dl_idx, G.cur_ep_idx);
|
||||||
|
|
||||||
|
// Check if result contains timestamp from Mp3Player
|
||||||
|
if (resultData) {
|
||||||
|
int32_t pos = 0;
|
||||||
|
bool has_pos = false;
|
||||||
|
if (tt_bundle_opt_int32(resultData, "pos_sec", &pos) ||
|
||||||
|
tt_bundle_opt_int32(resultData, "timestamp", &pos) ||
|
||||||
|
tt_bundle_opt_int32(resultData, "position", &pos)) {
|
||||||
|
has_pos = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
char file_path[512]={0};
|
||||||
|
bool has_file = tt_bundle_opt_string(resultData, "file", file_path, sizeof(file_path)) ||
|
||||||
|
tt_bundle_opt_string(resultData, "path", file_path, sizeof(file_path));
|
||||||
|
|
||||||
|
if (has_pos) {
|
||||||
|
ESP_LOGI(TAG, "Mp3Player returned pos %d sec for file %s", pos, has_file?file_path:"unknown");
|
||||||
|
int target_idx = -1;
|
||||||
|
if (has_file && strlen(file_path)>0) {
|
||||||
|
for(int i=0;i<G.ep_cnt;i++){
|
||||||
|
char ep_path[300];
|
||||||
|
make_sd_path(ep_path, sizeof(ep_path), G.eps[i].slug);
|
||||||
|
if (strcmp(ep_path, file_path)==0) { target_idx = i; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (target_idx<0 && G.pending_dl_idx>=0 && G.pending_dl_idx < G.ep_cnt) target_idx = G.pending_dl_idx;
|
||||||
|
if (target_idx<0 && G.cur_ep_idx>=0) target_idx = G.cur_ep_idx;
|
||||||
|
|
||||||
|
if (target_idx>=0) {
|
||||||
|
bool finished = false;
|
||||||
|
tt_bundle_opt_bool(resultData, "finished", &finished);
|
||||||
|
int32_t total_sec = 0;
|
||||||
|
tt_bundle_opt_int32(resultData, "total_sec", &total_sec);
|
||||||
|
if (finished || (total_sec>0 && pos >= total_sec-10)) {
|
||||||
|
set_saved_pos_for_slug(G.eps[target_idx].slug, 0);
|
||||||
|
G.pos_sec = 0;
|
||||||
|
ESP_LOGI(TAG, "Episode %s finished, clearing pos", G.eps[target_idx].slug);
|
||||||
|
} else {
|
||||||
|
set_saved_pos_for_slug(G.eps[target_idx].slug, pos);
|
||||||
|
G.pos_sec = pos;
|
||||||
|
ESP_LOGI(TAG, "Saved pos %d for %s", pos, G.eps[target_idx].slug);
|
||||||
|
}
|
||||||
|
save_state();
|
||||||
|
ui_rebuild_episode_list();
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if (G.lbl_status) {
|
||||||
|
if (pos>5) lv_label_set_text(G.lbl_status,"Progress saved – tap episode to resume");
|
||||||
|
else lv_label_set_text(G.lbl_status,"Select episode to play / download");
|
||||||
|
}
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
}
|
||||||
|
G.pending_dl_idx = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SdDownloader batch result handling
|
||||||
|
if (G.pending_dl_idx <0 || G.pending_dl_idx >= G.ep_cnt) {
|
||||||
|
if (resultData) {
|
||||||
|
int32_t succ=0, fail=0, total=0;
|
||||||
|
if (tt_bundle_opt_int32(resultData, "success_count", &succ) ||
|
||||||
|
tt_bundle_opt_int32(resultData, "total_count", &total)) {
|
||||||
|
tt_bundle_opt_int32(resultData, "fail_count", &fail);
|
||||||
|
ESP_LOGI(TAG, "Batch DL result: success %ld fail %ld total %ld", (long)succ, (long)fail, (long)total);
|
||||||
|
for(int i=0;i<G.ep_cnt;i++) G.eps[i].seen = has_slug(G.eps[i].slug);
|
||||||
|
save_state();
|
||||||
|
ui_rebuild_episode_list();
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if (G.lbl_status) {
|
||||||
|
char msg[64];
|
||||||
|
snprintf(msg,sizeof(msg),"DL: %ld ok, %ld fail", (long)succ, (long)fail);
|
||||||
|
lv_label_set_text(G.lbl_status, msg);
|
||||||
|
}
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
G.pending_dl_idx = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
G.pending_dl_idx = -1;
|
||||||
|
// Still refresh list in case files appeared
|
||||||
|
for(int i=0;i<G.ep_cnt;i++) G.eps[i].seen = has_slug(G.eps[i].slug);
|
||||||
|
ui_rebuild_episode_list();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single download result
|
||||||
|
bool success = false;
|
||||||
|
if (resultData) {
|
||||||
|
tt_bundle_opt_bool(resultData, "success", &success);
|
||||||
|
}
|
||||||
|
if (result == APP_RESULT_OK) {
|
||||||
|
if (resultData) {
|
||||||
|
bool b=false;
|
||||||
|
if (tt_bundle_opt_bool(resultData, "success", &b)) success = b;
|
||||||
|
else success = true;
|
||||||
|
} else {
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int idx = G.pending_dl_idx;
|
||||||
|
G.pending_dl_idx = -1;
|
||||||
|
|
||||||
|
for(int i=0;i<G.ep_cnt;i++) G.eps[i].seen = has_slug(G.eps[i].slug);
|
||||||
|
save_state();
|
||||||
|
ui_rebuild_episode_list();
|
||||||
|
|
||||||
|
// Handle sequential full-season download queue (safe single-file mode)
|
||||||
|
if(G.dl_missing_active){
|
||||||
|
if(success) G.dl_missing_done++;
|
||||||
|
else G.dl_missing_failed++;
|
||||||
|
|
||||||
|
int next = find_next_missing_in_season(G.dl_missing_season);
|
||||||
|
if(next!=-1){
|
||||||
|
ESP_LOGI(TAG, "Batch queue continuing: %d/%d done, next idx %d", G.dl_missing_done, G.dl_missing_total, next);
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if(G.lbl_status){
|
||||||
|
char msg[64];
|
||||||
|
snprintf(msg,sizeof(msg),"DL %d/%d S%02d...", G.dl_missing_done+1, G.dl_missing_total, G.dl_missing_season);
|
||||||
|
lv_label_set_text(G.lbl_status, msg);
|
||||||
|
}
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
// Small delay to let UI update before launching next downloader
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(300));
|
||||||
|
if(download_episode(next)){
|
||||||
|
// started next
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// failed to start next, abort queue
|
||||||
|
G.dl_missing_active=false;
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if(G.lbl_status){
|
||||||
|
char msg[64];
|
||||||
|
snprintf(msg,sizeof(msg),"DL stopped %d ok %d fail", G.dl_missing_done, G.dl_missing_failed);
|
||||||
|
lv_label_set_text(G.lbl_status, msg);
|
||||||
|
}
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No more missing – queue finished
|
||||||
|
ESP_LOGI(TAG, "Batch queue finished: %d ok %d fail", G.dl_missing_done, G.dl_missing_failed);
|
||||||
|
G.dl_missing_active=false;
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if(G.lbl_status){
|
||||||
|
char msg[64];
|
||||||
|
snprintf(msg,sizeof(msg),"DL S%02d done %d ok %d fail", G.dl_missing_season, G.dl_missing_done, G.dl_missing_failed);
|
||||||
|
lv_label_set_text(G.lbl_status, msg);
|
||||||
|
}
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
if(success && idx>=0) ESP_LOGI(TAG, "Single DL success idx %d", idx);
|
||||||
|
else ESP_LOGW(TAG, "External DL failed for idx %d", idx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if (G.lbl_status) {
|
||||||
|
if (success) lv_label_set_text(G.lbl_status, "DL done – tap to play");
|
||||||
|
else lv_label_set_text(G.lbl_status, "DL failed – tap to retry");
|
||||||
|
}
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
|
||||||
|
if(success && idx>=0){
|
||||||
|
ESP_LOGI(TAG, "Single DL success idx %d", idx);
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "External DL failed for idx %d", idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void onHide(AppHandle app, void* data){
|
||||||
|
(void)app; (void)data;
|
||||||
|
save_current_ep_pos();
|
||||||
|
G.fetch_cancel_req=true;
|
||||||
|
G.dl_cancel_req=true;
|
||||||
|
G.dl_missing_active=false;
|
||||||
|
|
||||||
|
if(G.fetch_handle){
|
||||||
|
ESP_LOGI(TAG,"onHide: waiting fetch to exit");
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
for(int i=0;i<20 && G.fetch_handle!=NULL; i++){
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(100));
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
}
|
||||||
|
if(G.fetch_handle){
|
||||||
|
ESP_LOGW(TAG,"onHide: force deleting fetch task (leak risk)");
|
||||||
|
vTaskDelete(G.fetch_handle);
|
||||||
|
G.fetch_handle=NULL;
|
||||||
|
}
|
||||||
|
G.fetching=false;
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(G.dl_handle){
|
||||||
|
ESP_LOGI(TAG,"onHide: waiting dl to exit");
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
for(int i=0;i<30 && G.dl_handle!=NULL; i++){
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(100));
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
}
|
||||||
|
if(G.dl_handle){
|
||||||
|
ESP_LOGW(TAG,"onHide: force deleting dl task after timeout");
|
||||||
|
vTaskDelete(G.dl_handle);
|
||||||
|
G.dl_handle=NULL;
|
||||||
|
G.downloading=false;
|
||||||
|
if(G.dl_overlay) hide_dl_overlay();
|
||||||
|
}
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
wait_play_exit();
|
||||||
|
ui_deinit();
|
||||||
|
close_overlay();
|
||||||
|
if(G.dl_overlay) hide_dl_overlay();
|
||||||
|
if(G.root){
|
||||||
|
lv_obj_delete(G.root);
|
||||||
|
G.root = NULL;
|
||||||
|
G.lbl_title = NULL;
|
||||||
|
G.lbl_meta = NULL;
|
||||||
|
G.lbl_season = NULL;
|
||||||
|
G.ep_list_cont = NULL;
|
||||||
|
G.lbl_time = NULL;
|
||||||
|
G.lbl_status = NULL;
|
||||||
|
G.btn_play = NULL;
|
||||||
|
G.btn_seasons = NULL;
|
||||||
|
G.btn_dl_missing = NULL;
|
||||||
|
G.btn_refresh = NULL;
|
||||||
|
G.season_list_cont = NULL;
|
||||||
|
G.bar = NULL;
|
||||||
|
}
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
save_state();
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]){
|
||||||
|
(void)argc; (void)argv;
|
||||||
|
tt_app_register((AppRegistration){ .onShow=onShow, .onHide=onHide, .onResult=onResult });
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,330 @@
|
|||||||
|
#include "network.h"
|
||||||
|
#include "storage.h"
|
||||||
|
#include "cJSON.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <lwip/sockets.h>
|
||||||
|
#include <lwip/inet.h>
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_heap_caps.h"
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
uint16_t my_htons(uint16_t v){
|
||||||
|
// Proper byte swap with masking to avoid overflow
|
||||||
|
return (uint16_t)(((v & 0x00FF) << 8) | ((v & 0xFF00) >> 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool resolve_host(const char* host, uint32_t* out_ip){
|
||||||
|
if(!host || !out_ip) return false;
|
||||||
|
uint32_t ip = ipaddr_addr(host);
|
||||||
|
if(ip!=0 && ip!=0xFFFFFFFF){
|
||||||
|
*out_ip=ip;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
ESP_LOGE(TAG,"resolve fail (DNS not supported) %s",host);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void normalize_audio_url(const char* in, char* out, size_t out_len){
|
||||||
|
if(!in || !out) return;
|
||||||
|
if(strncmp(in,"http://",7)==0){
|
||||||
|
strncpy(out,in,out_len-1);
|
||||||
|
out[out_len-1]=0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(in[0]=='/'){
|
||||||
|
snprintf(out,out_len,"%s%s",PB,in);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(strstr(in,"://")==NULL){
|
||||||
|
if(in[0]=='/') snprintf(out,out_len,"%s%s",PB,in);
|
||||||
|
else snprintf(out,out_len,"%s/%s",PB,in);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
strncpy(out,in,out_len-1);
|
||||||
|
out[out_len-1]=0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int http_get_raw(const char* url, char** out_body){
|
||||||
|
if(!url || strncmp(url,"http://",7)!=0){ ESP_LOGE(TAG,"http_get url not http %s",url); return -1; }
|
||||||
|
const char* p = url+7;
|
||||||
|
const char* slash = strchr(p,'/');
|
||||||
|
if(!slash){ ESP_LOGE(TAG,"no slash %s",url); return -1; }
|
||||||
|
char hostport[128]={0};
|
||||||
|
size_t hlen=slash-p;
|
||||||
|
if(hlen>=sizeof(hostport)) return -1;
|
||||||
|
memcpy(hostport,p,hlen);
|
||||||
|
hostport[hlen]=0;
|
||||||
|
const char* path=slash;
|
||||||
|
char host[96]={0};
|
||||||
|
int port=80;
|
||||||
|
char* colon=strchr(hostport,':');
|
||||||
|
if(colon){ *colon=0; strncpy(host,hostport,sizeof(host)-1); port=atoi(colon+1); if(port<=0) port=80; } else { strncpy(host,hostport,sizeof(host)-1); }
|
||||||
|
ESP_LOGI(TAG,"GET %s host=%s port=%d",url,host,port);
|
||||||
|
uint32_t ip=0;
|
||||||
|
ESP_LOGI(TAG,"Resolving host %s",host);
|
||||||
|
if(!resolve_host(host,&ip)){ ESP_LOGE(TAG,"ip fail %s",host); return -1; }
|
||||||
|
ESP_LOGI(TAG,"Resolved %s -> ip 0x%08x", host, ip);
|
||||||
|
int fd=lwip_socket(AF_INET,SOCK_STREAM,0);
|
||||||
|
ESP_LOGI(TAG,"Socket fd=%d", fd);
|
||||||
|
if(fd<0){ ESP_LOGE(TAG,"socket fail"); return -1; }
|
||||||
|
struct sockaddr_in sa;
|
||||||
|
ESP_LOGI(TAG,"Memset sa");
|
||||||
|
memset(&sa,0,sizeof(sa));
|
||||||
|
ESP_LOGI(TAG,"Set sa len/family/port/addr");
|
||||||
|
sa.sin_len = sizeof(sa);
|
||||||
|
sa.sin_family=AF_INET;
|
||||||
|
sa.sin_port=my_htons(port);
|
||||||
|
sa.sin_addr.s_addr=ip;
|
||||||
|
ESP_LOGI(TAG,"SetSockOpt RCV");
|
||||||
|
struct timeval tv={5,0};
|
||||||
|
int opt_res = lwip_setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv));
|
||||||
|
ESP_LOGI(TAG,"SetSockOpt RCV res=%d", opt_res);
|
||||||
|
ESP_LOGI(TAG,"SetSockOpt SND");
|
||||||
|
opt_res = lwip_setsockopt(fd,SOL_SOCKET,SO_SNDTIMEO,&tv,sizeof(tv));
|
||||||
|
ESP_LOGI(TAG,"SetSockOpt SND res=%d", opt_res);
|
||||||
|
ESP_LOGI(TAG,"Connecting to %s:%d",host,port);
|
||||||
|
int conn_res = lwip_connect(fd,(struct sockaddr*)&sa,sizeof(sa));
|
||||||
|
ESP_LOGI(TAG,"Connect res=%d", conn_res);
|
||||||
|
if(conn_res<0){ ESP_LOGE(TAG,"connect fail %s:%d",host,port); close(fd); return -1; }
|
||||||
|
ESP_LOGI(TAG,"Connected, sending request");
|
||||||
|
char req[512];
|
||||||
|
int reqlen=snprintf(req,sizeof(req),"GET %s HTTP/1.0\r\nHost: %s:%d\r\nConnection: close\r\nUser-Agent: TactilityDM/0.1\r\nAccept: */*\r\n\r\n",path,host,port);
|
||||||
|
if(lwip_send(fd,req,reqlen,0)<0){ ESP_LOGE(TAG,"send fail"); close(fd); return -1; }
|
||||||
|
ESP_LOGI(TAG,"Request sent, waiting for response");
|
||||||
|
|
||||||
|
int capacity=8192; // further reduced to 8K internal to avoid PSRAM StoreProhibited crash (seen 0x8208775e)
|
||||||
|
char* buf=malloc(capacity);
|
||||||
|
if(!buf){
|
||||||
|
buf=heap_caps_malloc(capacity, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||||
|
if(!buf){ ESP_LOGE(TAG,"buf 8k fail"); close(fd); return -1; }
|
||||||
|
}
|
||||||
|
int total=0;
|
||||||
|
char* header_end=NULL;
|
||||||
|
int body_start=0;
|
||||||
|
int content_len=-1;
|
||||||
|
bool is_chunked=false;
|
||||||
|
|
||||||
|
while(1){
|
||||||
|
// Graceful cancel support to avoid force vTaskDelete leaking PSRAM/SD resources
|
||||||
|
if(G.fetch_cancel_req){
|
||||||
|
ESP_LOGI(TAG,"http_get_raw canceled by user");
|
||||||
|
heap_caps_free(buf);
|
||||||
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if(total>=capacity-1){
|
||||||
|
int newcap=capacity*2;
|
||||||
|
if(newcap>600000){ ESP_LOGE(TAG,"too large %d",newcap); heap_caps_free(buf); close(fd); return -1; }
|
||||||
|
char* nb=heap_caps_malloc(newcap, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||||
|
if(!nb){ heap_caps_free(buf); close(fd); return -1; }
|
||||||
|
memcpy(nb,buf,total);
|
||||||
|
heap_caps_free(buf);
|
||||||
|
buf=nb;
|
||||||
|
capacity=newcap;
|
||||||
|
}
|
||||||
|
int r=lwip_recv(fd,buf+total,capacity-total-1,0);
|
||||||
|
if(r<=0){
|
||||||
|
if(r<0 && (errno==EAGAIN || errno==EWOULDBLOCK || errno==ETIMEDOUT)){
|
||||||
|
ESP_LOGW(TAG,"http_get_raw timeout total=%d",total);
|
||||||
|
// On timeout, if fetching canceled, abort
|
||||||
|
if(G.fetch_cancel_req){
|
||||||
|
ESP_LOGI(TAG,"http_get_raw timeout + canceled");
|
||||||
|
heap_caps_free(buf);
|
||||||
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// Continue loop – LWIP will retry until EOF
|
||||||
|
// But if we already have header and content_len satisfied, break
|
||||||
|
if(header_end && content_len>=0 && !is_chunked && total-body_start >= content_len) break;
|
||||||
|
// If no header yet and timeout, treat as failure to avoid 15s hang
|
||||||
|
if(!header_end && total==0){
|
||||||
|
ESP_LOGE(TAG,"http_get_raw timeout before header");
|
||||||
|
heap_caps_free(buf);
|
||||||
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// Otherwise continue to try again
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
total+=r;
|
||||||
|
buf[total]=0;
|
||||||
|
if(!header_end){
|
||||||
|
header_end=strstr(buf,"\r\n\r\n");
|
||||||
|
if(header_end){
|
||||||
|
body_start=(header_end-buf)+4;
|
||||||
|
if(strstr(buf,"chunked")) is_chunked=true;
|
||||||
|
char* cl=strstr(buf,"Content-Length:");
|
||||||
|
if(!cl) cl=strstr(buf,"content-length:");
|
||||||
|
if(cl){ cl=strchr(cl,':'); if(cl){ cl++; while(*cl==' ') cl++; content_len=atoi(cl); } }
|
||||||
|
ESP_LOGI(TAG,"header_end chunked=%d cl=%d total=%d",is_chunked,content_len,total);
|
||||||
|
if(content_len>=0 && !is_chunked){
|
||||||
|
if(total-body_start >= content_len) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(content_len>=0 && !is_chunked){
|
||||||
|
if(total-body_start >= content_len) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(fd);
|
||||||
|
if(!header_end){ ESP_LOGE(TAG,"no header end total=%d",total); heap_caps_free(buf); return -1; }
|
||||||
|
if(strncmp(buf,"HTTP/1.1 200",12)!=0 && strncmp(buf,"HTTP/1.0 200",12)!=0){ ESP_LOGE(TAG,"not 200: %.60s",buf); heap_caps_free(buf); return -1; }
|
||||||
|
|
||||||
|
int raw_len=total-body_start;
|
||||||
|
char* raw=buf+body_start;
|
||||||
|
char* final_body=NULL;
|
||||||
|
int final_len=0;
|
||||||
|
|
||||||
|
if(is_chunked){
|
||||||
|
final_body=heap_caps_malloc(200000, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||||
|
if(!final_body){ heap_caps_free(buf); return -1; }
|
||||||
|
int pos=0, out=0;
|
||||||
|
while(pos<raw_len){
|
||||||
|
char* crlf=strstr(raw+pos,"\r\n");
|
||||||
|
if(!crlf) break;
|
||||||
|
int off=crlf-(raw+pos);
|
||||||
|
if(off<=0 || off>16){ pos+=off+2; continue; }
|
||||||
|
char hex[16]={0};
|
||||||
|
memcpy(hex,raw+pos,off);
|
||||||
|
char* semi=strchr(hex,';');
|
||||||
|
if(semi) *semi=0;
|
||||||
|
long sz=strtol(hex,NULL,16);
|
||||||
|
if(sz==0) break;
|
||||||
|
pos+=off+2;
|
||||||
|
if(pos+sz>raw_len) sz=raw_len-pos;
|
||||||
|
if(out+sz>=200000) break;
|
||||||
|
memcpy(final_body+out,raw+pos,sz);
|
||||||
|
out+=sz;
|
||||||
|
pos+=sz;
|
||||||
|
if(pos+1<raw_len && raw[pos]=='\r' && raw[pos+1]=='\n') pos+=2;
|
||||||
|
}
|
||||||
|
final_body[out]=0;
|
||||||
|
final_len=out;
|
||||||
|
} else {
|
||||||
|
int bl=raw_len;
|
||||||
|
if(content_len>=0 && bl>content_len) bl=content_len;
|
||||||
|
final_body=heap_caps_malloc(bl+1, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||||
|
if(!final_body){ heap_caps_free(buf); return -1; }
|
||||||
|
memcpy(final_body,raw,bl);
|
||||||
|
final_body[bl]=0;
|
||||||
|
final_len=bl;
|
||||||
|
}
|
||||||
|
heap_caps_free(buf);
|
||||||
|
*out_body=final_body;
|
||||||
|
ESP_LOGI(TAG,"GET ok %d",final_len);
|
||||||
|
return final_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool fetch_data(){
|
||||||
|
char* js=NULL;
|
||||||
|
char url[320];
|
||||||
|
bool seasons_ok=false;
|
||||||
|
bool episodes_ok=false;
|
||||||
|
ESP_LOGI(TAG,"fetch seasons");
|
||||||
|
// Reduce payload via fields filter – drastically lowers cJSON malloc pressure (internal heap low warning)
|
||||||
|
snprintf(url,sizeof(url),"%s/api/collections/dm_seasons/records?perPage=100&sort=season_num&fields=season_num",PB);
|
||||||
|
int len=http_get_raw(url,&js);
|
||||||
|
if(len>0 && js){
|
||||||
|
cJSON* root=cJSON_Parse(js);
|
||||||
|
free(js); js=NULL;
|
||||||
|
if(root){
|
||||||
|
cJSON* items=cJSON_GetObjectItem(root,"items");
|
||||||
|
if(cJSON_IsArray(items)){
|
||||||
|
G.season_cnt=0;
|
||||||
|
int n=cJSON_GetArraySize(items);
|
||||||
|
for(int i=0;i<n&&G.season_cnt<MAX_SEASONS;i++){
|
||||||
|
cJSON* it=cJSON_GetArrayItem(items,i);
|
||||||
|
cJSON* sn=cJSON_GetObjectItem(it,"season_num");
|
||||||
|
if(sn&&cJSON_IsNumber(sn)){ G.seasons[G.season_cnt].num=sn->valueint; G.season_cnt++; }
|
||||||
|
}
|
||||||
|
sort_seasons();
|
||||||
|
if(G.season_cnt>0) seasons_ok=true;
|
||||||
|
}
|
||||||
|
cJSON_Delete(root);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG,"seasons fetch failed");
|
||||||
|
}
|
||||||
|
if(G.season_cnt==0){
|
||||||
|
ESP_LOGW(TAG,"seasons fallback");
|
||||||
|
for(int i=1;i<=37;i++) G.seasons[i-1].num=i;
|
||||||
|
G.season_cnt=37;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG,"fetch episodes paginated to reduce internal heap (prev low 7KB)");
|
||||||
|
G.ep_cnt=0;
|
||||||
|
for(int page=1; page<=10; page++){
|
||||||
|
if(G.fetch_cancel_req) break;
|
||||||
|
if(G.ep_cnt>=MAX_EPS) break;
|
||||||
|
snprintf(url,sizeof(url),"%s/api/collections/dm_episodes/records?perPage=25&page=%d&sort=season_num,episode_num&fields=title,slug,season_num,episode_num,audio_url",PB,page);
|
||||||
|
len=http_get_raw(url,&js);
|
||||||
|
if(len<=0 || !js){
|
||||||
|
ESP_LOGW(TAG,"episodes page %d fetch failed",page);
|
||||||
|
if(js){ free(js); js=NULL; }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cJSON* root=cJSON_Parse(js);
|
||||||
|
free(js); js=NULL;
|
||||||
|
if(!root){
|
||||||
|
ESP_LOGW(TAG,"episodes page %d JSON parse fail",page);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cJSON* items=cJSON_GetObjectItem(root,"items");
|
||||||
|
if(!cJSON_IsArray(items)){
|
||||||
|
cJSON_Delete(root);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
int n=cJSON_GetArraySize(items);
|
||||||
|
if(n==0){
|
||||||
|
cJSON_Delete(root);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG,"episodes page %d got %d items (total so far %d)",page,n,G.ep_cnt);
|
||||||
|
for(int i=0;i<n&&G.ep_cnt<MAX_EPS;i++){
|
||||||
|
cJSON* it=cJSON_GetArrayItem(items,i);
|
||||||
|
cJSON* t=cJSON_GetObjectItem(it,"title");
|
||||||
|
cJSON* slug=cJSON_GetObjectItem(it,"slug");
|
||||||
|
cJSON* sn=cJSON_GetObjectItem(it,"season_num");
|
||||||
|
cJSON* en=cJSON_GetObjectItem(it,"episode_num");
|
||||||
|
cJSON* au=cJSON_GetObjectItem(it,"audio_url");
|
||||||
|
if(!sn||!en) continue;
|
||||||
|
EpInfo* e=&G.eps[G.ep_cnt];
|
||||||
|
memset(e,0,sizeof(*e));
|
||||||
|
if(t&&cJSON_IsString(t)) strncpy(e->title,t->valuestring,sizeof(e->title)-1);
|
||||||
|
if(slug&&cJSON_IsString(slug)) strncpy(e->slug,slug->valuestring,sizeof(e->slug)-1);
|
||||||
|
else if(t) strncpy(e->slug,t->valuestring,sizeof(e->slug)-1);
|
||||||
|
e->season=sn->valueint;
|
||||||
|
e->ep_num=en->valueint;
|
||||||
|
if(au&&cJSON_IsString(au)) strncpy(e->audio_url,au->valuestring,sizeof(e->audio_url)-1);
|
||||||
|
e->seen=has_slug(e->slug);
|
||||||
|
G.ep_cnt++;
|
||||||
|
}
|
||||||
|
episodes_ok=true;
|
||||||
|
cJSON_Delete(root);
|
||||||
|
// Yield between pages to let MemoryChecker recover
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(400));
|
||||||
|
if(n<25) break; // last page
|
||||||
|
}
|
||||||
|
// Final sort after all pages
|
||||||
|
if(G.ep_cnt>0) sort_eps();
|
||||||
|
|
||||||
|
if(G.ep_cnt==0){
|
||||||
|
ESP_LOGW(TAG,"episodes fallback");
|
||||||
|
for(int i=0;i<6;i++){
|
||||||
|
EpInfo* e=&G.eps[i];
|
||||||
|
snprintf(e->slug,sizeof(e->slug),"S01E%02d",i+1);
|
||||||
|
snprintf(e->title,sizeof(e->title),"Episode %d",i+1);
|
||||||
|
e->season=1; e->ep_num=i+1;
|
||||||
|
}
|
||||||
|
G.ep_cnt=6;
|
||||||
|
}
|
||||||
|
for(int i=0;i<G.ep_cnt;i++) G.eps[i].seen=has_slug(G.eps[i].slug);
|
||||||
|
return (seasons_ok && episodes_ok);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "app_context.h"
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
uint16_t my_htons(uint16_t v);
|
||||||
|
bool resolve_host(const char* host, uint32_t* out_ip);
|
||||||
|
void normalize_audio_url(const char* in, char* out, size_t out_len);
|
||||||
|
|
||||||
|
// Robust HTTP GET with PSRAM buffer, returns length and allocates *out_body (must be free'd via free/heap_caps_free)
|
||||||
|
int http_get_raw(const char* url, char** out_body);
|
||||||
|
|
||||||
|
// Fetch seasons and episodes from PB – returns true if network fetch succeeded with real data
|
||||||
|
bool fetch_data(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
#include "player.h"
|
||||||
|
#include "storage.h"
|
||||||
|
#include "ui.h"
|
||||||
|
#include "download.h"
|
||||||
|
|
||||||
|
#include <tt_lvgl.h>
|
||||||
|
#include <tt_app.h>
|
||||||
|
#include <tt_bundle.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_heap_caps.h"
|
||||||
|
|
||||||
|
void wait_play_exit(void) {
|
||||||
|
// No internal playback anymore, nothing to wait
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop_playback_sync(void) {
|
||||||
|
// No internal playback, nothing to stop
|
||||||
|
G.is_playing = false;
|
||||||
|
G.is_paused = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool try_external_downloader(int idx) {
|
||||||
|
if(idx<0||idx>=G.ep_cnt) return false;
|
||||||
|
EpInfo* ep=&G.eps[idx];
|
||||||
|
if (strlen(ep->audio_url) < 8) {
|
||||||
|
ESP_LOGW(TAG, "Skipping DL for %s: empty URL", ep->slug);
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if (G.lbl_status) lv_label_set_text(G.lbl_status, "No URL – fetch failed?");
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
char fpath[300];
|
||||||
|
make_sd_path(fpath, sizeof(fpath), ep->slug);
|
||||||
|
|
||||||
|
// Preserve browsing season so we return to same season after downloader closes – survive G memset via static
|
||||||
|
dm_preserve_season(ep->season);
|
||||||
|
int saved_season = G.cur_season;
|
||||||
|
G.cur_season = ep->season;
|
||||||
|
save_state();
|
||||||
|
G.cur_ep_idx = idx;
|
||||||
|
(void)saved_season;
|
||||||
|
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
close_overlay();
|
||||||
|
if(G.dl_overlay) hide_dl_overlay();
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
|
||||||
|
BundleHandle b = tt_bundle_alloc();
|
||||||
|
if (!b) {
|
||||||
|
ESP_LOGW(TAG, "Failed to alloc bundle for external DL");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
tt_bundle_put_string(b, "url", ep->audio_url);
|
||||||
|
tt_bundle_put_string(b, "path", fpath);
|
||||||
|
tt_bundle_put_bool(b, "override", false);
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Starting external SdDownloader for %s -> %s season %d", ep->slug, fpath, G.cur_season);
|
||||||
|
G.pending_dl_idx = idx;
|
||||||
|
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if (G.lbl_status) lv_label_set_text(G.lbl_status, "Starting downloader...");
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
|
||||||
|
tt_app_start_with_bundle("one.tactility.sddownloader", b);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int count_missing_in_season(int season){
|
||||||
|
int cnt=0;
|
||||||
|
for(int i=0;i<G.ep_cnt;i++){
|
||||||
|
if(G.eps[i].season==season && !G.eps[i].seen && strlen(G.eps[i].audio_url)>=8) cnt++;
|
||||||
|
}
|
||||||
|
return cnt;
|
||||||
|
}
|
||||||
|
int find_next_missing_in_season(int season){
|
||||||
|
for(int i=0;i<G.ep_cnt;i++){
|
||||||
|
if(G.eps[i].season==season && !G.eps[i].seen && strlen(G.eps[i].audio_url)>=8) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
bool download_episode(int idx){
|
||||||
|
return try_external_downloader(idx);
|
||||||
|
}
|
||||||
|
bool download_missing_next(void){
|
||||||
|
if(!G.dl_missing_active) return false;
|
||||||
|
int next = find_next_missing_in_season(G.dl_missing_season);
|
||||||
|
if(next==-1){
|
||||||
|
// No more missing – done
|
||||||
|
G.dl_missing_active=false;
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if(G.lbl_status){
|
||||||
|
char msg[64];
|
||||||
|
snprintf(msg,sizeof(msg),"DL S%02d done %d ok %d fail", G.dl_missing_season, G.dl_missing_done, G.dl_missing_failed);
|
||||||
|
lv_label_set_text(G.lbl_status, msg);
|
||||||
|
}
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Update UI for next file
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if(G.lbl_status){
|
||||||
|
char msg[64];
|
||||||
|
snprintf(msg,sizeof(msg),"DL %d/%d S%02d...", G.dl_missing_done+1, G.dl_missing_total, G.dl_missing_season);
|
||||||
|
lv_label_set_text(G.lbl_status, msg);
|
||||||
|
}
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
return try_external_downloader(next);
|
||||||
|
}
|
||||||
|
bool download_missing_season_start(int season){
|
||||||
|
int missing = count_missing_in_season(season);
|
||||||
|
if(missing==0){
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if(G.lbl_status) lv_label_set_text(G.lbl_status,"Season already cached");
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
G.dl_missing_active=false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "Starting sequential DL for S%02d, %d missing", season, missing);
|
||||||
|
G.dl_missing_active=true;
|
||||||
|
G.dl_missing_season=season;
|
||||||
|
G.dl_missing_total=missing;
|
||||||
|
G.dl_missing_done=0;
|
||||||
|
G.dl_missing_failed=0;
|
||||||
|
// Preserve season in persisted state and static before leaving
|
||||||
|
dm_preserve_season(season);
|
||||||
|
G.cur_season = season;
|
||||||
|
save_state();
|
||||||
|
int first = find_next_missing_in_season(season);
|
||||||
|
if(first==-1){
|
||||||
|
G.dl_missing_active=false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Update status
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if(G.lbl_status){
|
||||||
|
char msg[64];
|
||||||
|
snprintf(msg,sizeof(msg),"DL 1/%d S%02d...", missing, season);
|
||||||
|
lv_label_set_text(G.lbl_status, msg);
|
||||||
|
}
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
return try_external_downloader(first);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool try_external_downloader_batch(int season) {
|
||||||
|
// Sequential queue – safe single-file mode
|
||||||
|
return download_missing_season_start(season);
|
||||||
|
}
|
||||||
|
|
||||||
|
void start_idx_internal(int idx){
|
||||||
|
if(idx<0||idx>=G.ep_cnt) return;
|
||||||
|
EpInfo* ep=&G.eps[idx];
|
||||||
|
char fpath[300];
|
||||||
|
make_sd_path(fpath, sizeof(fpath), ep->slug);
|
||||||
|
|
||||||
|
if(!has_slug(ep->slug)){
|
||||||
|
ESP_LOGW(TAG, "start_idx_internal called but file missing %s", ep->slug);
|
||||||
|
start_idx(idx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int saved = 0;
|
||||||
|
if(G.cur_ep_idx!=idx){
|
||||||
|
saved = get_saved_pos_for_slug(ep->slug);
|
||||||
|
if(saved>5){
|
||||||
|
ESP_LOGI(TAG,"Restoring saved pos %d for %s", saved, ep->slug);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
saved = G.pos_sec;
|
||||||
|
}
|
||||||
|
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
close_overlay();
|
||||||
|
if(G.dl_overlay) hide_dl_overlay();
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
|
||||||
|
BundleHandle b = tt_bundle_alloc();
|
||||||
|
if(b){
|
||||||
|
tt_bundle_put_string(b, "file", fpath);
|
||||||
|
tt_bundle_put_string(b, "path", fpath);
|
||||||
|
tt_bundle_put_int32(b, "pos_sec", saved);
|
||||||
|
tt_bundle_put_int32(b, "position", saved);
|
||||||
|
tt_bundle_put_int32(b, "volume", G.volume);
|
||||||
|
ESP_LOGI(TAG, "Launching Mp3Player for %s pos %d season %d", fpath, saved, ep->season);
|
||||||
|
G.pending_dl_idx = idx;
|
||||||
|
G.cur_ep_idx=idx; G.cur_season=ep->season;
|
||||||
|
strncpy(G.cur_title,ep->title,sizeof(G.cur_title)-1);
|
||||||
|
strncpy(G.last_slug,ep->slug,sizeof(G.last_slug)-1);
|
||||||
|
dm_preserve_season(ep->season);
|
||||||
|
save_state();
|
||||||
|
tt_app_start_with_bundle("one.tactility.mp3player", b);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGW(TAG, "Failed to alloc bundle for Mp3Player");
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if(G.lbl_status) lv_label_set_text(G.lbl_status,"Failed to launch player");
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
void start_idx(int idx){
|
||||||
|
if(idx<0||idx>=G.ep_cnt) return;
|
||||||
|
EpInfo* ep=&G.eps[idx];
|
||||||
|
if(!has_slug(ep->slug)){
|
||||||
|
// File not on SD, download via external downloader
|
||||||
|
if (try_external_downloader(idx)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Fallback: show message, cannot download internally anymore
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if(G.lbl_status) lv_label_set_text(G.lbl_status,"Install SdDownloader");
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
start_idx_internal(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void select_ep(int idx){
|
||||||
|
if(idx<0||idx>=G.ep_cnt) return;
|
||||||
|
if(G.cur_ep_idx>=0 && G.cur_ep_idx < G.ep_cnt && G.cur_ep_idx!=idx){
|
||||||
|
save_current_ep_pos();
|
||||||
|
}
|
||||||
|
G.cur_ep_idx=idx;
|
||||||
|
EpInfo* ep=&G.eps[idx];
|
||||||
|
G.cur_season=ep->season;
|
||||||
|
strncpy(G.last_slug, ep->slug, sizeof(G.last_slug)-1);
|
||||||
|
|
||||||
|
int saved = get_saved_pos_for_slug(ep->slug);
|
||||||
|
G.pos_sec=saved;
|
||||||
|
G.last_pct=-1;
|
||||||
|
G.total_sec=0;
|
||||||
|
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if(G.lbl_title) lv_label_set_text(G.lbl_title,ep->title);
|
||||||
|
if(G.lbl_meta){
|
||||||
|
char b[64];
|
||||||
|
if(saved>5){
|
||||||
|
char tbuf[16];
|
||||||
|
fmt_time(tbuf,saved);
|
||||||
|
snprintf(b,sizeof(b),"S%02d E%02d %s • %s",ep->season,ep->ep_num,ep->seen?"*":"",tbuf);
|
||||||
|
} else {
|
||||||
|
snprintf(b,sizeof(b),"S%02d E%02d %s",ep->season,ep->ep_num,ep->seen?"*":"");
|
||||||
|
}
|
||||||
|
lv_label_set_text(G.lbl_meta,b);
|
||||||
|
}
|
||||||
|
if(G.lbl_status){
|
||||||
|
if(saved>5) lv_label_set_text(G.lbl_status,"Resume – tap Play");
|
||||||
|
else lv_label_set_text(G.lbl_status,ep->seen?"On SD – tap Play":"Tap Play to download");
|
||||||
|
}
|
||||||
|
if(G.bar){
|
||||||
|
lv_bar_set_value(G.bar,0,LV_ANIM_OFF);
|
||||||
|
}
|
||||||
|
if(G.lbl_time){
|
||||||
|
if(saved>5){
|
||||||
|
char a[16];
|
||||||
|
fmt_time(a,saved);
|
||||||
|
char buf[40];
|
||||||
|
snprintf(buf,sizeof(buf),"%s / --:--",a);
|
||||||
|
lv_label_set_text(G.lbl_time,buf);
|
||||||
|
} else {
|
||||||
|
lv_label_set_text(G.lbl_time,"0:00 / --:--");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
save_state();
|
||||||
|
}
|
||||||
|
|
||||||
|
void go_next(){
|
||||||
|
if(G.ep_cnt==0) return;
|
||||||
|
save_current_ep_pos();
|
||||||
|
save_state();
|
||||||
|
int n=G.cur_ep_idx+1;
|
||||||
|
if(n>=G.ep_cnt) n=0;
|
||||||
|
select_ep(n);
|
||||||
|
// Do not auto-play next, per user request: no autoplay
|
||||||
|
}
|
||||||
|
|
||||||
|
void go_prev(){
|
||||||
|
if(G.ep_cnt==0) return;
|
||||||
|
if(G.pos_sec>5){
|
||||||
|
if(G.cur_ep_idx>=0){
|
||||||
|
set_saved_pos_for_slug(G.eps[G.cur_ep_idx].slug, 0);
|
||||||
|
save_state();
|
||||||
|
}
|
||||||
|
G.pos_sec=0;
|
||||||
|
int cur=G.cur_ep_idx;
|
||||||
|
if(cur<0) cur=0;
|
||||||
|
select_ep(cur);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
save_current_ep_pos();
|
||||||
|
save_state();
|
||||||
|
int p=G.cur_ep_idx-1;
|
||||||
|
if(p<0) p=G.ep_cnt-1;
|
||||||
|
select_ep(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from UI for season batch download
|
||||||
|
bool download_season(int season) {
|
||||||
|
return try_external_downloader_batch(season);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "app_context.h"
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Simplified: no internal audio, only external apps
|
||||||
|
void wait_play_exit(void);
|
||||||
|
void stop_playback_sync(void);
|
||||||
|
|
||||||
|
void start_idx_internal(int idx);
|
||||||
|
void start_idx(int idx);
|
||||||
|
void select_ep(int idx);
|
||||||
|
void go_next(void);
|
||||||
|
void go_prev(void);
|
||||||
|
bool download_season(int season);
|
||||||
|
|
||||||
|
// New single-file download API
|
||||||
|
bool download_episode(int idx);
|
||||||
|
bool download_missing_season_start(int season);
|
||||||
|
bool download_missing_next(void); // continue queue, returns false if done
|
||||||
|
int count_missing_in_season(int season);
|
||||||
|
int find_next_missing_in_season(int season);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
#include "storage.h"
|
||||||
|
#include "cJSON.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include "esp_heap_caps.h"
|
||||||
|
|
||||||
|
void make_sd_path(char* out, size_t out_len, const char* slug){
|
||||||
|
if(!out || !slug) return;
|
||||||
|
snprintf(out, out_len, "%s/%s.mp3", DM_DIR, slug);
|
||||||
|
}
|
||||||
|
void ensure_dir(){
|
||||||
|
mkdir("/sdcard/apps",0777);
|
||||||
|
mkdir("/sdcard/apps/one.tactility.discoverymountain",0777);
|
||||||
|
mkdir("/sdcard/apps/one.tactility.discoverymountain/userdata",0777);
|
||||||
|
mkdir(DM_DIR,0777);
|
||||||
|
}
|
||||||
|
bool has_slug(const char* slug){
|
||||||
|
char p[300];
|
||||||
|
make_sd_path(p, sizeof(p), slug);
|
||||||
|
struct stat st;
|
||||||
|
return stat(p,&st)==0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void fmt_time(char* o,int s){ snprintf(o,16,"%d:%02d",s/60,s%60); }
|
||||||
|
|
||||||
|
void sort_seasons(){
|
||||||
|
for(int i=0;i<G.season_cnt;i++){
|
||||||
|
for(int j=i+1;j<G.season_cnt;j++){
|
||||||
|
if(G.seasons[j].num < G.seasons[i].num){
|
||||||
|
SeasonInfo t=G.seasons[i]; G.seasons[i]=G.seasons[j]; G.seasons[j]=t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void sort_eps(){
|
||||||
|
// Use PSRAM for temp to avoid stack overflow (EpInfo ~300 bytes, was on stack)
|
||||||
|
EpInfo *t = heap_caps_malloc(sizeof(EpInfo), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||||
|
if(!t) return;
|
||||||
|
for(int i=0;i<G.ep_cnt;i++){
|
||||||
|
for(int j=i+1;j<G.ep_cnt;j++){
|
||||||
|
EpInfo *a=&G.eps[i], *b=&G.eps[j];
|
||||||
|
bool should_swap=false;
|
||||||
|
if(a->season != b->season) should_swap = (b->season < a->season);
|
||||||
|
else should_swap = (b->ep_num < a->ep_num);
|
||||||
|
if(should_swap){
|
||||||
|
*t=*a; *a=*b; *b=*t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
heap_caps_free(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Per-episode position helpers ──
|
||||||
|
int find_saved_pos_idx(const char* slug){
|
||||||
|
if(!slug) return -1;
|
||||||
|
for(int i=0;i<G.saved_pos_cnt;i++){
|
||||||
|
if(strcmp(G.saved_pos[i].slug, slug)==0) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int get_saved_pos_for_slug(const char* slug){
|
||||||
|
int idx=find_saved_pos_idx(slug);
|
||||||
|
if(idx>=0) return G.saved_pos[idx].pos;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
void set_saved_pos_for_slug(const char* slug, int pos){
|
||||||
|
if(!slug || strlen(slug)==0) return;
|
||||||
|
if(pos<0) pos=0;
|
||||||
|
if(G.total_sec>0 && pos>0 && pos >= G.total_sec-10){
|
||||||
|
pos=0;
|
||||||
|
}
|
||||||
|
int idx=find_saved_pos_idx(slug);
|
||||||
|
if(idx>=0){
|
||||||
|
G.saved_pos[idx].pos=pos;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(G.saved_pos_cnt < MAX_EPS){
|
||||||
|
strncpy(G.saved_pos[G.saved_pos_cnt].slug, slug, sizeof(G.saved_pos[G.saved_pos_cnt].slug)-1);
|
||||||
|
G.saved_pos[G.saved_pos_cnt].slug[sizeof(G.saved_pos[G.saved_pos_cnt].slug)-1]=0;
|
||||||
|
G.saved_pos[G.saved_pos_cnt].pos=pos;
|
||||||
|
G.saved_pos_cnt++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void save_current_ep_pos(){
|
||||||
|
if(G.cur_ep_idx>=0 && G.cur_ep_idx < G.ep_cnt){
|
||||||
|
const char* slug=G.eps[G.cur_ep_idx].slug;
|
||||||
|
set_saved_pos_for_slug(slug, G.pos_sec);
|
||||||
|
} else if(strlen(G.last_slug)>0){
|
||||||
|
set_saved_pos_for_slug(G.last_slug, G.pos_sec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void save_state(){
|
||||||
|
if(G.cur_ep_idx>=0 && G.ep_cnt>0){
|
||||||
|
if(G.pos_sec>=0){
|
||||||
|
set_saved_pos_for_slug(G.eps[G.cur_ep_idx].slug, G.pos_sec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cJSON* r=cJSON_CreateObject();
|
||||||
|
if(G.ep_cnt>0 && G.cur_ep_idx>=0 && G.cur_ep_idx < G.ep_cnt) cJSON_AddStringToObject(r,"last_slug",G.eps[G.cur_ep_idx].slug);
|
||||||
|
else if(strlen(G.last_slug)>0) cJSON_AddStringToObject(r,"last_slug",G.last_slug);
|
||||||
|
cJSON_AddNumberToObject(r,"last_season",G.cur_season);
|
||||||
|
cJSON_AddNumberToObject(r,"pos_sec",G.pos_sec);
|
||||||
|
if(G.saved_pos_cnt>0){
|
||||||
|
cJSON* pos_obj=cJSON_CreateObject();
|
||||||
|
for(int i=0;i<G.saved_pos_cnt;i++){
|
||||||
|
if(G.saved_pos[i].slug[0]==0) continue;
|
||||||
|
if(G.saved_pos[i].pos>0){
|
||||||
|
cJSON_AddNumberToObject(pos_obj, G.saved_pos[i].slug, G.saved_pos[i].pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cJSON_AddItemToObject(r,"positions",pos_obj);
|
||||||
|
}
|
||||||
|
char* s=cJSON_PrintUnformatted(r);
|
||||||
|
if(s){
|
||||||
|
FILE* f=fopen(STATE_PATH,"w");
|
||||||
|
if(f){ fwrite(s,1,strlen(s),f); fclose(f);}
|
||||||
|
free(s);
|
||||||
|
}
|
||||||
|
cJSON_Delete(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
void load_state(){
|
||||||
|
G.cur_season=1; G.cur_ep_idx=-1; G.pos_sec=0;
|
||||||
|
G.saved_pos_cnt=0;
|
||||||
|
G.last_save_tick=0;
|
||||||
|
memset(G.last_slug,0,sizeof(G.last_slug));
|
||||||
|
memset(G.saved_pos,0,sizeof(G.saved_pos));
|
||||||
|
FILE* f=fopen(STATE_PATH,"r");
|
||||||
|
if(!f) return;
|
||||||
|
fseek(f,0,SEEK_END);
|
||||||
|
long sz=ftell(f);
|
||||||
|
fseek(f,0,SEEK_SET);
|
||||||
|
if(sz<=0||sz>16384){ fclose(f); return;}
|
||||||
|
char* buf=malloc(sz+1);
|
||||||
|
if(!buf){ fclose(f); return; }
|
||||||
|
size_t got=fread(buf,1,sz,f);
|
||||||
|
buf[got]=0;
|
||||||
|
fclose(f);
|
||||||
|
cJSON* r=cJSON_Parse(buf);
|
||||||
|
free(buf);
|
||||||
|
if(!r) return;
|
||||||
|
cJSON* ls=cJSON_GetObjectItem(r,"last_slug");
|
||||||
|
cJSON* lsn=cJSON_GetObjectItem(r,"last_season");
|
||||||
|
cJSON* ps=cJSON_GetObjectItem(r,"pos_sec");
|
||||||
|
if(ps&&cJSON_IsNumber(ps)) G.pos_sec=ps->valueint;
|
||||||
|
if(lsn&&cJSON_IsNumber(lsn)) G.cur_season=lsn->valueint;
|
||||||
|
if(ls&&cJSON_IsString(ls)){
|
||||||
|
strncpy(G.last_slug, ls->valuestring, sizeof(G.last_slug)-1);
|
||||||
|
// Do NOT overwrite cur_season here based on slug – browsing season is independent
|
||||||
|
// cur_ep_idx will be resolved after cache load in onShow
|
||||||
|
}
|
||||||
|
cJSON* poss=cJSON_GetObjectItem(r,"positions");
|
||||||
|
if(poss && cJSON_IsObject(poss)){
|
||||||
|
cJSON* child=NULL;
|
||||||
|
cJSON_ArrayForEach(child, poss){
|
||||||
|
if(cJSON_IsNumber(child) && child->string && G.saved_pos_cnt < MAX_EPS){
|
||||||
|
strncpy(G.saved_pos[G.saved_pos_cnt].slug, child->string, sizeof(G.saved_pos[G.saved_pos_cnt].slug)-1);
|
||||||
|
G.saved_pos[G.saved_pos_cnt].pos=child->valueint;
|
||||||
|
G.saved_pos_cnt++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(strlen(G.last_slug)>0){
|
||||||
|
int sp=get_saved_pos_for_slug(G.last_slug);
|
||||||
|
if(sp>0) G.pos_sec=sp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cJSON_Delete(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool save_cache(void){
|
||||||
|
if(G.ep_cnt==0 || G.season_cnt==0) return false;
|
||||||
|
cJSON* root=cJSON_CreateObject();
|
||||||
|
if(!root) return false;
|
||||||
|
cJSON* seasons_arr=cJSON_CreateArray();
|
||||||
|
if(!seasons_arr){ cJSON_Delete(root); return false; }
|
||||||
|
for(int i=0;i<G.season_cnt && i<MAX_SEASONS;i++){
|
||||||
|
cJSON_AddItemToArray(seasons_arr, cJSON_CreateNumber(G.seasons[i].num));
|
||||||
|
}
|
||||||
|
cJSON_AddItemToObject(root,"seasons",seasons_arr);
|
||||||
|
|
||||||
|
cJSON* eps_arr=cJSON_CreateArray();
|
||||||
|
if(!eps_arr){ cJSON_Delete(root); return false; }
|
||||||
|
for(int i=0;i<G.ep_cnt && i<MAX_EPS;i++){
|
||||||
|
EpInfo* e=&G.eps[i];
|
||||||
|
if(e->slug[0]==0) continue;
|
||||||
|
cJSON* obj=cJSON_CreateObject();
|
||||||
|
if(!obj) continue;
|
||||||
|
cJSON_AddStringToObject(obj,"title", e->title);
|
||||||
|
cJSON_AddStringToObject(obj,"slug", e->slug);
|
||||||
|
cJSON_AddNumberToObject(obj,"season_num", e->season);
|
||||||
|
cJSON_AddNumberToObject(obj,"episode_num", e->ep_num);
|
||||||
|
cJSON_AddStringToObject(obj,"audio_url", e->audio_url);
|
||||||
|
cJSON_AddItemToArray(eps_arr, obj);
|
||||||
|
}
|
||||||
|
cJSON_AddItemToObject(root,"episodes",eps_arr);
|
||||||
|
|
||||||
|
char* s=cJSON_PrintUnformatted(root);
|
||||||
|
cJSON_Delete(root);
|
||||||
|
if(!s) return false;
|
||||||
|
FILE* f=fopen(CACHE_PATH,"w");
|
||||||
|
bool ok=false;
|
||||||
|
if(f){
|
||||||
|
size_t len=strlen(s);
|
||||||
|
size_t wrote=fwrite(s,1,len,f);
|
||||||
|
fclose(f);
|
||||||
|
ok = (wrote==len);
|
||||||
|
}
|
||||||
|
free(s);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool load_cache(void){
|
||||||
|
FILE* f=fopen(CACHE_PATH,"r");
|
||||||
|
if(!f) return false;
|
||||||
|
fseek(f,0,SEEK_END);
|
||||||
|
long sz=ftell(f);
|
||||||
|
fseek(f,0,SEEK_SET);
|
||||||
|
if(sz<=0 || sz>200000){ fclose(f); return false; }
|
||||||
|
// Use PSRAM for large json to avoid internal heap pressure
|
||||||
|
char* buf=heap_caps_malloc(sz+1, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||||
|
if(!buf){
|
||||||
|
buf=malloc(sz+1);
|
||||||
|
if(!buf){ fclose(f); return false; }
|
||||||
|
}
|
||||||
|
size_t got=fread(buf,1,sz,f);
|
||||||
|
buf[got]=0;
|
||||||
|
fclose(f);
|
||||||
|
cJSON* root=cJSON_Parse(buf);
|
||||||
|
heap_caps_free(buf);
|
||||||
|
if(!root) return false;
|
||||||
|
|
||||||
|
cJSON* seasons_arr=cJSON_GetObjectItem(root,"seasons");
|
||||||
|
cJSON* eps_arr=cJSON_GetObjectItem(root,"episodes");
|
||||||
|
if(!cJSON_IsArray(seasons_arr) || !cJSON_IsArray(eps_arr)){
|
||||||
|
cJSON_Delete(root);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int scnt=cJSON_GetArraySize(seasons_arr);
|
||||||
|
G.season_cnt=0;
|
||||||
|
for(int i=0;i<scnt && G.season_cnt<MAX_SEASONS;i++){
|
||||||
|
cJSON* it=cJSON_GetArrayItem(seasons_arr,i);
|
||||||
|
if(cJSON_IsNumber(it)){
|
||||||
|
G.seasons[G.season_cnt].num=it->valueint;
|
||||||
|
G.season_cnt++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int ecnt=cJSON_GetArraySize(eps_arr);
|
||||||
|
G.ep_cnt=0;
|
||||||
|
for(int i=0;i<ecnt && G.ep_cnt<MAX_EPS;i++){
|
||||||
|
cJSON* it=cJSON_GetArrayItem(eps_arr,i);
|
||||||
|
if(!cJSON_IsObject(it)) continue;
|
||||||
|
cJSON* t=cJSON_GetObjectItem(it,"title");
|
||||||
|
cJSON* slug=cJSON_GetObjectItem(it,"slug");
|
||||||
|
cJSON* sn=cJSON_GetObjectItem(it,"season_num");
|
||||||
|
cJSON* en=cJSON_GetObjectItem(it,"episode_num");
|
||||||
|
cJSON* au=cJSON_GetObjectItem(it,"audio_url");
|
||||||
|
if(!sn || !en) continue;
|
||||||
|
EpInfo* e=&G.eps[G.ep_cnt];
|
||||||
|
memset(e,0,sizeof(*e));
|
||||||
|
if(t && cJSON_IsString(t)) strncpy(e->title, t->valuestring, sizeof(e->title)-1);
|
||||||
|
if(slug && cJSON_IsString(slug)) strncpy(e->slug, slug->valuestring, sizeof(e->slug)-1);
|
||||||
|
else if(t && cJSON_IsString(t)) strncpy(e->slug, t->valuestring, sizeof(e->slug)-1);
|
||||||
|
e->season=sn->valueint;
|
||||||
|
e->ep_num=en->valueint;
|
||||||
|
if(au && cJSON_IsString(au)) strncpy(e->audio_url, au->valuestring, sizeof(e->audio_url)-1);
|
||||||
|
e->seen=has_slug(e->slug);
|
||||||
|
G.ep_cnt++;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_Delete(root);
|
||||||
|
if(G.season_cnt>0) sort_seasons();
|
||||||
|
if(G.ep_cnt>0) sort_eps();
|
||||||
|
// Refresh seen in case SD changed
|
||||||
|
for(int i=0;i<G.ep_cnt;i++) G.eps[i].seen=has_slug(G.eps[i].slug);
|
||||||
|
return (G.season_cnt>0 && G.ep_cnt>0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void lru_check(){
|
||||||
|
typedef struct{ char path[300]; time_t mt; } FE;
|
||||||
|
// Allocate from PSRAM to avoid stack overflow (200*~304=60KB on stack would overflow 12KB task)
|
||||||
|
FE *files = heap_caps_malloc(200 * sizeof(FE), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||||
|
if(!files) return;
|
||||||
|
int fc=0;
|
||||||
|
DIR* d=opendir(DM_DIR);
|
||||||
|
if(!d){ heap_caps_free(files); return; }
|
||||||
|
struct dirent* de;
|
||||||
|
char cur_path[300]={0};
|
||||||
|
if(G.cur_ep_idx>=0 && G.cur_ep_idx<G.ep_cnt){
|
||||||
|
make_sd_path(cur_path, sizeof(cur_path), G.eps[G.cur_ep_idx].slug);
|
||||||
|
}
|
||||||
|
while((de=readdir(d))!=NULL && fc<200){
|
||||||
|
if(de->d_name[0]=='.') continue;
|
||||||
|
size_t l=strlen(de->d_name);
|
||||||
|
if(l<5) continue;
|
||||||
|
if(strcmp(de->d_name+l-4,".mp3")!=0) continue;
|
||||||
|
char full[300];
|
||||||
|
snprintf(full,sizeof(full),"%s/%.200s",DM_DIR,de->d_name);
|
||||||
|
struct stat st;
|
||||||
|
if(stat(full,&st)!=0) continue;
|
||||||
|
if(cur_path[0] && strcmp(full,cur_path)==0) continue;
|
||||||
|
strncpy(files[fc].path,full,sizeof(files[fc].path)-1);
|
||||||
|
files[fc].mt=st.st_mtime;
|
||||||
|
fc++;
|
||||||
|
}
|
||||||
|
closedir(d);
|
||||||
|
if(fc<12){ heap_caps_free(files); return; }
|
||||||
|
for(int i=0;i<fc;i++){
|
||||||
|
for(int j=i+1;j<fc;j++){
|
||||||
|
if(files[j].mt<files[i].mt){ FE t=files[i]; files[i]=files[j]; files[j]=t; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int todel=fc-8;
|
||||||
|
for(int i=0;i<todel;i++) unlink(files[i].path);
|
||||||
|
heap_caps_free(files);
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "app_context.h"
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Path helpers
|
||||||
|
void make_sd_path(char* out, size_t out_len, const char* slug);
|
||||||
|
void ensure_dir(void);
|
||||||
|
bool has_slug(const char* slug);
|
||||||
|
|
||||||
|
// LRU cache cleanup
|
||||||
|
void lru_check(void);
|
||||||
|
|
||||||
|
// Sorting
|
||||||
|
void sort_seasons(void);
|
||||||
|
void sort_eps(void);
|
||||||
|
|
||||||
|
// Time formatting
|
||||||
|
void fmt_time(char* o, int s);
|
||||||
|
|
||||||
|
// Per-episode resume helpers
|
||||||
|
int find_saved_pos_idx(const char* slug);
|
||||||
|
int get_saved_pos_for_slug(const char* slug);
|
||||||
|
void set_saved_pos_for_slug(const char* slug, int pos);
|
||||||
|
void save_current_ep_pos(void);
|
||||||
|
|
||||||
|
// Persistence
|
||||||
|
void save_state(void);
|
||||||
|
void load_state(void);
|
||||||
|
|
||||||
|
// Seasons / episodes cache (PB data, rarely changes)
|
||||||
|
bool save_cache(void);
|
||||||
|
bool load_cache(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,587 @@
|
|||||||
|
#include "ui.h"
|
||||||
|
#include "storage.h"
|
||||||
|
#include "player.h"
|
||||||
|
#include "download.h"
|
||||||
|
|
||||||
|
#include <tt_lvgl.h>
|
||||||
|
#include <tt_app.h>
|
||||||
|
#include <tt_bundle.h>
|
||||||
|
#include <tactility/lvgl_fonts.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_heap_caps.h"
|
||||||
|
|
||||||
|
void close_overlay(){
|
||||||
|
if(G.overlay){
|
||||||
|
lv_obj_delete(G.overlay);
|
||||||
|
G.overlay=NULL;
|
||||||
|
G.dropdown=NULL;
|
||||||
|
G.season_list_cont=NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void overlay_close_timer_cb(lv_timer_t* t){
|
||||||
|
close_overlay();
|
||||||
|
lv_timer_delete(t);
|
||||||
|
}
|
||||||
|
static void close_overlay_safe(void){
|
||||||
|
// Defer deletion to avoid deleting ancestor of current event target (season buttons inside overlay)
|
||||||
|
lv_timer_create(overlay_close_timer_cb, 20, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward decls
|
||||||
|
static void ep_action_cb(lv_event_t* e);
|
||||||
|
static void season_select_cb(lv_event_t* e);
|
||||||
|
static void rebuild_episode_list_locked(void);
|
||||||
|
|
||||||
|
static void clear_container(lv_obj_t* cont){
|
||||||
|
if(!cont) return;
|
||||||
|
uint32_t child_cnt = lv_obj_get_child_cnt(cont);
|
||||||
|
while(child_cnt>0){
|
||||||
|
lv_obj_t* child = lv_obj_get_child(cont, 0);
|
||||||
|
if(child) lv_obj_delete(child);
|
||||||
|
else break;
|
||||||
|
child_cnt = lv_obj_get_child_cnt(cont);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void btn_close_cb(lv_event_t* e){
|
||||||
|
(void)e;
|
||||||
|
if(G.overlay){
|
||||||
|
close_overlay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tt_app_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void get_season_stats(int season, int* total, int* cached){
|
||||||
|
int t=0,c=0;
|
||||||
|
for(int i=0;i<G.ep_cnt;i++){
|
||||||
|
if(G.eps[i].season==season){
|
||||||
|
t++;
|
||||||
|
if(G.eps[i].seen) c++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(total) *total=t;
|
||||||
|
if(cached) *cached=c;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_update_status_locked(const char* msg){
|
||||||
|
if(G.lbl_status && msg) lv_label_set_text(G.lbl_status, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_update_header_locked(void){
|
||||||
|
if(!G.lbl_title && !G.lbl_season) return;
|
||||||
|
if(G.fetching){
|
||||||
|
// Keep showing current season stats instead of blocking header
|
||||||
|
// status bar already says fetching
|
||||||
|
}
|
||||||
|
if(G.season_cnt==0){
|
||||||
|
if(G.lbl_season) lv_label_set_text(G.lbl_season, "No seasons");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int total=0,cached=0;
|
||||||
|
get_season_stats(G.cur_season,&total,&cached);
|
||||||
|
char buf[64];
|
||||||
|
if(total>0){
|
||||||
|
snprintf(buf,sizeof(buf),"S%02d • %d/%d on SD • %d eps", G.cur_season, cached, total, total);
|
||||||
|
}else{
|
||||||
|
snprintf(buf,sizeof(buf),"S%02d • %d eps", G.cur_season, total);
|
||||||
|
}
|
||||||
|
if(G.lbl_season) lv_label_set_text(G.lbl_season, buf);
|
||||||
|
if(G.lbl_title){
|
||||||
|
// Keep app name
|
||||||
|
// optionally update title to count
|
||||||
|
lv_label_set_text(G.lbl_title, "Discovery Mountain");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void rebuild_episode_list_locked(void){
|
||||||
|
if(!G.ep_list_cont) return;
|
||||||
|
clear_container(G.ep_list_cont);
|
||||||
|
|
||||||
|
if(G.ep_cnt==0){
|
||||||
|
if(G.fetching){
|
||||||
|
lv_obj_t* lbl=lv_label_create(G.ep_list_cont);
|
||||||
|
lv_label_set_text(lbl,"Fetching episodes...");
|
||||||
|
lv_obj_set_style_text_color(lbl, lv_color_hex(0xAAAAAA),0);
|
||||||
|
lv_obj_align(lbl,LV_ALIGN_TOP_MID,0,20);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lv_obj_t* lbl=lv_label_create(G.ep_list_cont);
|
||||||
|
lv_label_set_text(lbl,"No episodes");
|
||||||
|
lv_obj_set_style_text_color(lbl, lv_color_hex(0xAAAAAA),0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count episodes for cur season
|
||||||
|
int count=0;
|
||||||
|
for(int i=0;i<G.ep_cnt;i++) if(G.eps[i].season==G.cur_season) count++;
|
||||||
|
|
||||||
|
if(count==0){
|
||||||
|
lv_obj_t* lbl=lv_label_create(G.ep_list_cont);
|
||||||
|
lv_label_set_text(lbl,"No episodes in season");
|
||||||
|
lv_obj_set_style_text_color(lbl, lv_color_hex(0xAAAAAA),0);
|
||||||
|
// Also show all if fallback?
|
||||||
|
// list first few from all
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int i=0;i<G.ep_cnt;i++){
|
||||||
|
if(G.eps[i].season!=G.cur_season) continue;
|
||||||
|
EpInfo* ep=&G.eps[i];
|
||||||
|
|
||||||
|
// Row container – optimized for low internal heap (4 objs per ep)
|
||||||
|
lv_obj_t* row=lv_obj_create(G.ep_list_cont);
|
||||||
|
lv_obj_set_size(row, LV_PCT(100), 50);
|
||||||
|
lv_obj_set_style_bg_color(row, ep->seen ? lv_color_hex(0x1E2A22) : lv_color_hex(0x222222),0);
|
||||||
|
lv_obj_set_style_bg_opa(row, LV_OPA_COVER,0);
|
||||||
|
lv_obj_set_style_radius(row,8,0);
|
||||||
|
lv_obj_set_style_pad_all(row,6,0);
|
||||||
|
lv_obj_set_style_border_width(row,1,0);
|
||||||
|
lv_obj_set_style_border_color(row, ep->seen ? lv_color_hex(0x2E7D32) : lv_color_hex(0x333333),0);
|
||||||
|
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_set_style_pad_gap(row,6,0);
|
||||||
|
lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
lv_obj_add_event_cb(row, ep_action_cb, LV_EVENT_CLICKED, (void*)(intptr_t)i);
|
||||||
|
|
||||||
|
// Left single label with title (truncated) – we embed meta as second line via \n to save objects
|
||||||
|
lv_obj_t* ltitle=lv_label_create(row);
|
||||||
|
lv_obj_set_flex_grow(ltitle,1);
|
||||||
|
// Truncate title to ~26 chars for display
|
||||||
|
char short_title[30];
|
||||||
|
strncpy(short_title, ep->title, sizeof(short_title)-1);
|
||||||
|
short_title[sizeof(short_title)-1]=0;
|
||||||
|
if(strlen(ep->title) > 26){
|
||||||
|
short_title[23]='.';
|
||||||
|
short_title[24]='.';
|
||||||
|
short_title[25]='.';
|
||||||
|
short_title[26]=0;
|
||||||
|
}
|
||||||
|
char line1[64];
|
||||||
|
snprintf(line1,sizeof(line1),"E%02d: %s", ep->ep_num, short_title);
|
||||||
|
// Second line status
|
||||||
|
char line2[32];
|
||||||
|
int saved = get_saved_pos_for_slug(ep->slug);
|
||||||
|
if(ep->seen){
|
||||||
|
if(saved>5){
|
||||||
|
char tb[16]; fmt_time(tb,saved);
|
||||||
|
snprintf(line2,sizeof(line2),"On SD %s", tb);
|
||||||
|
}else{
|
||||||
|
snprintf(line2,sizeof(line2),"On SD");
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
snprintf(line2,sizeof(line2),"Not DL");
|
||||||
|
}
|
||||||
|
char tbuf[96];
|
||||||
|
snprintf(tbuf,sizeof(tbuf),"%s\n%s", line1, line2);
|
||||||
|
lv_label_set_text(ltitle, tbuf);
|
||||||
|
lv_obj_set_width(ltitle, LV_PCT(100));
|
||||||
|
lv_label_set_long_mode(ltitle, LV_LABEL_LONG_DOT);
|
||||||
|
lv_obj_set_style_text_font(ltitle, lvgl_get_text_font(FONT_SIZE_SMALL),0);
|
||||||
|
lv_obj_set_style_text_color(ltitle, ep->seen ? lv_color_hex(0xE0E0E0) : lv_color_hex(0xCCCCCC),0);
|
||||||
|
|
||||||
|
// Right side: button
|
||||||
|
lv_obj_t* btn=lv_btn_create(row);
|
||||||
|
lv_obj_set_size(btn, 52, 34);
|
||||||
|
if(ep->seen){
|
||||||
|
lv_obj_set_style_bg_color(btn, lv_color_hex(0x2E7D32),0);
|
||||||
|
}else{
|
||||||
|
lv_obj_set_style_bg_color(btn, lv_color_hex(0x1E88E5),0);
|
||||||
|
}
|
||||||
|
lv_obj_set_style_radius(btn,6,0);
|
||||||
|
lv_obj_t* bl=lv_label_create(btn);
|
||||||
|
lv_label_set_text(bl, ep->seen ? LV_SYMBOL_PLAY : LV_SYMBOL_DOWNLOAD);
|
||||||
|
lv_obj_center(bl);
|
||||||
|
lv_obj_add_event_cb(btn, ep_action_cb, LV_EVENT_CLICKED, (void*)(intptr_t)i);
|
||||||
|
}
|
||||||
|
// Scroll to top after rebuild
|
||||||
|
lv_obj_scroll_to_y(G.ep_list_cont, 0, LV_ANIM_OFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_rebuild_episode_list(void){
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if(G.ep_list_cont) rebuild_episode_list_locked();
|
||||||
|
ui_update_header_locked();
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ep_action_cb(lv_event_t* e){
|
||||||
|
if(lv_event_get_code(e)!=LV_EVENT_CLICKED) return;
|
||||||
|
// Retrieve idx from user data
|
||||||
|
int idx = (int)(intptr_t)lv_event_get_user_data(e);
|
||||||
|
// Fallback: try event target's user data via current target?
|
||||||
|
// lv_event_get_user_data should already give us the idx
|
||||||
|
if(idx<0 || idx>=G.ep_cnt) return;
|
||||||
|
if(G.downloading) return;
|
||||||
|
// Allow interaction even while fetching – cache remains usable
|
||||||
|
|
||||||
|
EpInfo* ep=&G.eps[idx];
|
||||||
|
ESP_LOGI(TAG,"ep_action idx %d S%02dE%02d seen %d", idx, ep->season, ep->ep_num, ep->seen);
|
||||||
|
|
||||||
|
if(ep->season != G.cur_season){
|
||||||
|
// Shouldn't happen but update cur season to match
|
||||||
|
G.cur_season = ep->season;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use start_idx which handles both DL and Play
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if(G.lbl_status){
|
||||||
|
if(ep->seen) lv_label_set_text(G.lbl_status,"Starting player...");
|
||||||
|
else lv_label_set_text(G.lbl_status,"Starting downloader...");
|
||||||
|
}
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
|
||||||
|
start_idx(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void season_select_cb(lv_event_t* e){
|
||||||
|
if(lv_event_get_code(e)!=LV_EVENT_CLICKED) return;
|
||||||
|
int season_num = (int)(intptr_t)lv_event_get_user_data(e);
|
||||||
|
if(season_num<=0) return;
|
||||||
|
ESP_LOGI(TAG,"Season selected S%02d", season_num);
|
||||||
|
// Defer overlay deletion – deleting ancestor during event is unsafe
|
||||||
|
close_overlay_safe();
|
||||||
|
G.cur_season = season_num;
|
||||||
|
// Save state immediately
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
ui_update_header_locked();
|
||||||
|
rebuild_episode_list_locked();
|
||||||
|
if(G.lbl_status){
|
||||||
|
char buf[40];
|
||||||
|
snprintf(buf,sizeof(buf),"Switched to S%02d", season_num);
|
||||||
|
lv_label_set_text(G.lbl_status, buf);
|
||||||
|
}
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
save_state();
|
||||||
|
|
||||||
|
// Jump to first unseen or first in season
|
||||||
|
int first=-1, first_unseen=-1;
|
||||||
|
for(int i=0;i<G.ep_cnt;i++) if(G.eps[i].season==season_num){
|
||||||
|
if(first==-1) first=i;
|
||||||
|
if(!G.eps[i].seen && first_unseen==-1) first_unseen=i;
|
||||||
|
}
|
||||||
|
int tgt = first_unseen!=-1?first_unseen:first;
|
||||||
|
if(tgt!=-1){
|
||||||
|
// Optional: select ep updates title but we already rebuilt list; keep cur_ep_idx
|
||||||
|
G.cur_ep_idx = tgt;
|
||||||
|
strncpy(G.last_slug, G.eps[tgt].slug, sizeof(G.last_slug)-1);
|
||||||
|
save_state();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void dropdown_cb(lv_event_t* e){
|
||||||
|
if(lv_event_get_code(e)!=LV_EVENT_VALUE_CHANGED) return;
|
||||||
|
lv_obj_t* dd = (lv_obj_t*)lv_event_get_target(e);
|
||||||
|
int sel=lv_dropdown_get_selected(dd);
|
||||||
|
if(sel<0||sel>=G.season_cnt) return;
|
||||||
|
int sn=G.seasons[sel].num;
|
||||||
|
close_overlay_safe();
|
||||||
|
int first=-1, first_unseen=-1;
|
||||||
|
for(int i=0;i<G.ep_cnt;i++) if(G.eps[i].season==sn){
|
||||||
|
if(first==-1) first=i;
|
||||||
|
if(!G.eps[i].seen && first_unseen==-1) first_unseen=i;
|
||||||
|
}
|
||||||
|
int tgt=first_unseen!=-1?first_unseen:first;
|
||||||
|
if(tgt!=-1){
|
||||||
|
G.cur_season=sn;
|
||||||
|
G.cur_ep_idx=tgt;
|
||||||
|
strncpy(G.last_slug, G.eps[tgt].slug, sizeof(G.last_slug)-1);
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
ui_update_header_locked();
|
||||||
|
rebuild_episode_list_locked();
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
save_state();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void close_btn_cb(lv_event_t* e){
|
||||||
|
(void)e;
|
||||||
|
close_overlay_safe();
|
||||||
|
}
|
||||||
|
|
||||||
|
void btn_refresh_cb(lv_event_t* e){
|
||||||
|
(void)e;
|
||||||
|
if(G.downloading) return;
|
||||||
|
if(G.overlay){ close_overlay(); }
|
||||||
|
if(G.fetching){
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if(G.lbl_status) lv_label_set_text(G.lbl_status,"Already refreshing...");
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG,"Manual refresh pressed");
|
||||||
|
trigger_manual_refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void btn_seasons_cb(lv_event_t* e){
|
||||||
|
(void)e;
|
||||||
|
if(G.downloading) return;
|
||||||
|
if(G.overlay){ close_overlay(); return; }
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
// Full-screen season selector – parent to active screen to cover whole 240x320, not just root content
|
||||||
|
lv_obj_t* ovl_parent = lv_scr_act();
|
||||||
|
G.overlay=lv_obj_create(ovl_parent);
|
||||||
|
lv_obj_set_size(G.overlay, LV_PCT(100), LV_PCT(100));
|
||||||
|
lv_obj_set_pos(G.overlay, 0, 0);
|
||||||
|
lv_obj_set_style_bg_color(G.overlay, lv_color_hex(0x111111),0);
|
||||||
|
lv_obj_set_style_bg_opa(G.overlay, LV_OPA_COVER,0);
|
||||||
|
lv_obj_set_style_radius(G.overlay,0,0);
|
||||||
|
lv_obj_set_style_border_width(G.overlay,0,0);
|
||||||
|
lv_obj_set_style_pad_all(G.overlay,6,0);
|
||||||
|
lv_obj_set_style_pad_gap(G.overlay,6,0);
|
||||||
|
lv_obj_set_flex_flow(G.overlay, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_flex_align(G.overlay, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
|
||||||
|
lv_obj_remove_flag(G.overlay, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
|
||||||
|
// Header row: title + close
|
||||||
|
lv_obj_t* hdr=lv_obj_create(G.overlay);
|
||||||
|
lv_obj_set_size(hdr, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_style_bg_opa(hdr, LV_OPA_TRANSP,0);
|
||||||
|
lv_obj_set_style_border_width(hdr,0,0);
|
||||||
|
lv_obj_set_style_pad_all(hdr,2,0);
|
||||||
|
lv_obj_set_flex_flow(hdr, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_flex_align(hdr, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
lv_obj_remove_flag(hdr, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
|
||||||
|
lv_obj_t* title=lv_label_create(hdr);
|
||||||
|
lv_label_set_text(title,"Select Season");
|
||||||
|
lv_obj_set_style_text_font(title, lvgl_get_text_font(FONT_SIZE_DEFAULT),0);
|
||||||
|
lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF),0);
|
||||||
|
lv_obj_set_flex_grow(title,1);
|
||||||
|
|
||||||
|
lv_obj_t* bc=lv_btn_create(hdr);
|
||||||
|
lv_obj_set_size(bc, 40, 36);
|
||||||
|
lv_obj_set_style_bg_color(bc, lv_color_hex(0x333333),0);
|
||||||
|
lv_obj_set_style_radius(bc,6,0);
|
||||||
|
lv_obj_t* lc=lv_label_create(bc);
|
||||||
|
lv_label_set_text(lc, LV_SYMBOL_CLOSE);
|
||||||
|
lv_obj_set_style_text_font(lc, lvgl_get_text_font(FONT_SIZE_SMALL),0);
|
||||||
|
lv_obj_center(lc);
|
||||||
|
lv_obj_add_event_cb(bc,close_btn_cb,LV_EVENT_CLICKED,NULL);
|
||||||
|
|
||||||
|
// Scrollable list container fills rest
|
||||||
|
G.season_list_cont=lv_obj_create(G.overlay);
|
||||||
|
lv_obj_set_size(G.season_list_cont, LV_PCT(100), LV_PCT(100));
|
||||||
|
lv_obj_set_flex_grow(G.season_list_cont,1);
|
||||||
|
lv_obj_set_style_bg_color(G.season_list_cont, lv_color_hex(0x0E0E0E),0);
|
||||||
|
lv_obj_set_style_radius(G.season_list_cont,8,0);
|
||||||
|
lv_obj_set_style_pad_all(G.season_list_cont,6,0);
|
||||||
|
lv_obj_set_flex_flow(G.season_list_cont, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_style_pad_gap(G.season_list_cont,6,0);
|
||||||
|
lv_obj_add_flag(G.season_list_cont, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_set_scrollbar_mode(G.season_list_cont, LV_SCROLLBAR_MODE_AUTO);
|
||||||
|
|
||||||
|
for(int i=0;i<G.season_cnt;i++){
|
||||||
|
int sn=G.seasons[i].num;
|
||||||
|
int total,cached;
|
||||||
|
get_season_stats(sn,&total,&cached);
|
||||||
|
lv_obj_t* btn=lv_btn_create(G.season_list_cont);
|
||||||
|
lv_obj_set_size(btn, LV_PCT(100), 52);
|
||||||
|
lv_obj_set_style_bg_color(btn, sn==G.cur_season ? lv_color_hex(0x2E7D32) : lv_color_hex(0x222222),0);
|
||||||
|
lv_obj_set_style_radius(btn,8,0);
|
||||||
|
lv_obj_set_style_pad_all(btn,10,0);
|
||||||
|
lv_obj_set_style_border_width(btn,1,0);
|
||||||
|
lv_obj_set_style_border_color(btn, sn==G.cur_season ? lv_color_hex(0x4CAF50) : lv_color_hex(0x333333),0);
|
||||||
|
lv_obj_add_event_cb(btn, season_select_cb, LV_EVENT_CLICKED, (void*)(intptr_t)sn);
|
||||||
|
lv_obj_set_flex_flow(btn, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_flex_align(btn, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
|
||||||
|
char blabel[48];
|
||||||
|
if(total>0){
|
||||||
|
snprintf(blabel,sizeof(blabel),"Season %02d %d/%d on SD", sn, cached, total);
|
||||||
|
}else{
|
||||||
|
snprintf(blabel,sizeof(blabel),"Season %02d %d eps", sn, total);
|
||||||
|
}
|
||||||
|
lv_obj_t* l1=lv_label_create(btn);
|
||||||
|
lv_label_set_text(l1, blabel);
|
||||||
|
lv_obj_set_style_text_font(l1, lvgl_get_text_font(FONT_SIZE_DEFAULT),0);
|
||||||
|
lv_obj_set_flex_grow(l1,1);
|
||||||
|
|
||||||
|
if(sn==G.cur_season){
|
||||||
|
lv_obj_t* lcheck=lv_label_create(btn);
|
||||||
|
lv_label_set_text(lcheck, LV_SYMBOL_OK);
|
||||||
|
lv_obj_set_style_text_color(lcheck, lv_color_hex(0x4CAF50),0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
void btn_dl_season_cb(lv_event_t* e){
|
||||||
|
(void)e;
|
||||||
|
if(G.downloading) return;
|
||||||
|
if(G.ep_cnt==0) return;
|
||||||
|
|
||||||
|
int season = G.cur_season;
|
||||||
|
ESP_LOGI(TAG, "DL Missing S%02d pressed", season);
|
||||||
|
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
close_overlay();
|
||||||
|
if(G.dl_overlay) hide_dl_overlay();
|
||||||
|
if(G.lbl_status){
|
||||||
|
char buf[40];
|
||||||
|
snprintf(buf,sizeof(buf),"Checking S%02d missing...", season);
|
||||||
|
lv_label_set_text(G.lbl_status, buf);
|
||||||
|
}
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
|
||||||
|
if (!download_season(season)) {
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
if(G.lbl_status) lv_label_set_text(G.lbl_status,"DL Season failed – no missing?");
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void build_ui(lv_obj_t* parent){
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
lv_obj_t* root = lv_obj_create(parent);
|
||||||
|
G.root = root;
|
||||||
|
lv_obj_set_size(root,LV_PCT(100),LV_PCT(100));
|
||||||
|
lv_obj_set_style_bg_color(root,lv_color_hex(0x111111),0);
|
||||||
|
lv_obj_set_style_pad_all(root,6,0);
|
||||||
|
lv_obj_set_style_pad_gap(root,6,0);
|
||||||
|
lv_obj_set_style_border_width(root,0,0);
|
||||||
|
lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_flex_align(root, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
|
||||||
|
lv_obj_remove_flag(root, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
|
||||||
|
// Header container
|
||||||
|
lv_obj_t* header=lv_obj_create(root);
|
||||||
|
lv_obj_set_size(header, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_style_bg_opa(header, LV_OPA_TRANSP,0);
|
||||||
|
lv_obj_set_style_border_width(header,0,0);
|
||||||
|
lv_obj_set_style_pad_all(header,2,0);
|
||||||
|
lv_obj_set_style_pad_gap(header,6,0);
|
||||||
|
lv_obj_set_flex_flow(header, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_flex_align(header, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
lv_obj_remove_flag(header, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
|
||||||
|
lv_obj_t* header_left=lv_obj_create(header);
|
||||||
|
lv_obj_set_size(header_left, LV_PCT(80), LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_style_bg_opa(header_left, LV_OPA_TRANSP,0);
|
||||||
|
lv_obj_set_style_border_width(header_left,0,0);
|
||||||
|
lv_obj_set_style_pad_all(header_left,0,0);
|
||||||
|
lv_obj_set_flex_flow(header_left, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_style_pad_gap(header_left,2,0);
|
||||||
|
lv_obj_remove_flag(header_left, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_set_flex_grow(header_left,1);
|
||||||
|
|
||||||
|
G.lbl_title=lv_label_create(header_left);
|
||||||
|
lv_label_set_text(G.lbl_title,"Discovery Mountain");
|
||||||
|
lv_obj_set_style_text_font(G.lbl_title, lvgl_get_text_font(FONT_SIZE_DEFAULT), 0);
|
||||||
|
lv_obj_set_style_text_color(G.lbl_title, lv_color_hex(0xFFFFFF),0);
|
||||||
|
|
||||||
|
G.lbl_season=lv_label_create(header_left);
|
||||||
|
lv_label_set_text(G.lbl_season,"Loading seasons...");
|
||||||
|
lv_obj_set_style_text_font(G.lbl_season, lvgl_get_text_font(FONT_SIZE_SMALL),0);
|
||||||
|
lv_obj_set_style_text_color(G.lbl_season,lv_color_hex(0xAAAAAA),0);
|
||||||
|
|
||||||
|
// Close button top right
|
||||||
|
lv_obj_t* btn_close=lv_btn_create(header);
|
||||||
|
lv_obj_set_size(btn_close, 36, 32);
|
||||||
|
lv_obj_set_style_bg_color(btn_close, lv_color_hex(0x333333),0);
|
||||||
|
lv_obj_set_style_radius(btn_close,6,0);
|
||||||
|
lv_obj_t* l_close=lv_label_create(btn_close);
|
||||||
|
lv_label_set_text(l_close, LV_SYMBOL_CLOSE);
|
||||||
|
lv_obj_set_style_text_font(l_close, lvgl_get_text_font(FONT_SIZE_SMALL),0);
|
||||||
|
lv_obj_center(l_close);
|
||||||
|
lv_obj_add_event_cb(btn_close, btn_close_cb, LV_EVENT_CLICKED, NULL);
|
||||||
|
|
||||||
|
// Action row: Seasons + Refresh (DL Missing removed per UX simplification)
|
||||||
|
lv_obj_t* row=lv_obj_create(root);
|
||||||
|
lv_obj_set_size(row,LV_PCT(100),44);
|
||||||
|
lv_obj_set_flex_flow(row,LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_flex_align(row,LV_FLEX_ALIGN_SPACE_EVENLY,LV_FLEX_ALIGN_CENTER,LV_FLEX_ALIGN_CENTER);
|
||||||
|
lv_obj_set_style_bg_opa(row,LV_OPA_TRANSP,0);
|
||||||
|
lv_obj_set_style_border_width(row,0,0);
|
||||||
|
lv_obj_set_style_pad_all(row,0,0);
|
||||||
|
lv_obj_set_style_pad_gap(row,8,0);
|
||||||
|
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
|
||||||
|
lv_obj_t* bs=lv_btn_create(row);
|
||||||
|
lv_obj_set_size(bs,LV_PCT(48),36);
|
||||||
|
G.btn_seasons=bs;
|
||||||
|
lv_obj_set_style_bg_color(bs, lv_color_hex(0x333333),0);
|
||||||
|
lv_obj_set_style_radius(bs,6,0);
|
||||||
|
lv_obj_t* ls=lv_label_create(bs);
|
||||||
|
lv_label_set_text(ls,"Seasons");
|
||||||
|
lv_obj_set_style_text_font(ls, lvgl_get_text_font(FONT_SIZE_SMALL),0);
|
||||||
|
lv_obj_center(ls);
|
||||||
|
lv_obj_add_event_cb(bs,btn_seasons_cb,LV_EVENT_CLICKED,NULL);
|
||||||
|
|
||||||
|
lv_obj_t* br=lv_btn_create(row);
|
||||||
|
lv_obj_set_size(br,LV_PCT(48),36);
|
||||||
|
G.btn_refresh=br;
|
||||||
|
lv_obj_set_style_bg_color(br, lv_color_hex(0x444444), 0);
|
||||||
|
lv_obj_set_style_radius(br,6,0);
|
||||||
|
lv_obj_t* lr=lv_label_create(br);
|
||||||
|
lv_label_set_text(lr, LV_SYMBOL_REFRESH " Refresh");
|
||||||
|
lv_obj_set_style_text_font(lr, lvgl_get_text_font(FONT_SIZE_SMALL),0);
|
||||||
|
lv_obj_center(lr);
|
||||||
|
lv_obj_add_event_cb(br,btn_refresh_cb,LV_EVENT_CLICKED,NULL);
|
||||||
|
|
||||||
|
G.btn_dl_missing=NULL;
|
||||||
|
|
||||||
|
// Episode list container – fills remaining space
|
||||||
|
G.ep_list_cont=lv_obj_create(root);
|
||||||
|
lv_obj_set_size(G.ep_list_cont, LV_PCT(100), LV_PCT(100));
|
||||||
|
lv_obj_set_flex_grow(G.ep_list_cont, 1);
|
||||||
|
lv_obj_set_style_bg_color(G.ep_list_cont, lv_color_hex(0x0E0E0E),0);
|
||||||
|
lv_obj_set_style_bg_opa(G.ep_list_cont, LV_OPA_COVER,0);
|
||||||
|
lv_obj_set_style_radius(G.ep_list_cont,8,0);
|
||||||
|
lv_obj_set_style_pad_all(G.ep_list_cont,4,0);
|
||||||
|
lv_obj_set_style_pad_gap(G.ep_list_cont,4,0);
|
||||||
|
lv_obj_set_flex_flow(G.ep_list_cont, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_flex_align(G.ep_list_cont, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
|
||||||
|
// Ensure scrollable
|
||||||
|
lv_obj_add_flag(G.ep_list_cont, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_set_scrollbar_mode(G.ep_list_cont, LV_SCROLLBAR_MODE_AUTO);
|
||||||
|
|
||||||
|
lv_obj_t* hint=lv_label_create(G.ep_list_cont);
|
||||||
|
lv_label_set_text(hint,"Loading episodes...");
|
||||||
|
lv_obj_set_style_text_color(hint, lv_color_hex(0x888888),0);
|
||||||
|
lv_obj_set_style_text_font(hint, lvgl_get_text_font(FONT_SIZE_SMALL),0);
|
||||||
|
|
||||||
|
// Status bar removed per UX request – no help label
|
||||||
|
G.lbl_status=NULL;
|
||||||
|
|
||||||
|
// Legacy placeholders nulled
|
||||||
|
G.lbl_meta=NULL;
|
||||||
|
G.bar=NULL;
|
||||||
|
G.lbl_time=NULL;
|
||||||
|
G.btn_play=NULL;
|
||||||
|
G.dl_overlay=NULL;
|
||||||
|
G.ui_timer_handle=NULL;
|
||||||
|
|
||||||
|
G.ui_timer_handle = lv_timer_create(ui_timer,300,NULL);
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_deinit(void){
|
||||||
|
if(G.ui_timer_handle){
|
||||||
|
lv_timer_delete(G.ui_timer_handle);
|
||||||
|
G.ui_timer_handle=NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_timer(lv_timer_t* t){
|
||||||
|
(void)t;
|
||||||
|
// If app is closing (root null) skip
|
||||||
|
if(!G.root) return;
|
||||||
|
if(G.fetching){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(G.downloading && G.dl_overlay){
|
||||||
|
update_dl_overlay_ui();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void btn_play_cb(lv_event_t* e){ (void)e; /* legacy, not used */ }
|
||||||
|
void btn_prev_cb(lv_event_t* e){ (void)e; /* legacy */ }
|
||||||
|
void btn_next_cb(lv_event_t* e){ (void)e; /* legacy */ }
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "app_context.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void build_ui(lv_obj_t* parent);
|
||||||
|
void ui_timer(lv_timer_t* t);
|
||||||
|
|
||||||
|
void btn_play_cb(lv_event_t* e);
|
||||||
|
void btn_prev_cb(lv_event_t* e);
|
||||||
|
void btn_next_cb(lv_event_t* e);
|
||||||
|
void btn_seasons_cb(lv_event_t* e);
|
||||||
|
void btn_dl_season_cb(lv_event_t* e);
|
||||||
|
void btn_refresh_cb(lv_event_t* e);
|
||||||
|
void close_overlay(void);
|
||||||
|
void dropdown_cb(lv_event_t* e);
|
||||||
|
|
||||||
|
// New library UI helpers
|
||||||
|
void ui_rebuild_episode_list(void);
|
||||||
|
void ui_update_header_locked(void);
|
||||||
|
void ui_update_status_locked(const char* msg);
|
||||||
|
void ui_deinit(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
manifest.version=0.2
|
||||||
|
target.sdk=0.8.0-dev
|
||||||
|
target.platforms=esp32s3
|
||||||
|
app.id=one.tactility.discoverymountain
|
||||||
|
app.name=Discovery Mountain
|
||||||
|
app.version.name=0.1.0
|
||||||
|
app.version.code=1
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
[manifest]
|
manifest.version=0.2
|
||||||
version=0.1
|
target.sdk=0.8.0-dev
|
||||||
[target]
|
target.platforms=esp32s3,esp32p4
|
||||||
sdk=0.7.0-dev
|
app.id=one.tactility.epubreader
|
||||||
platforms=esp32s3,esp32p4
|
app.version.name=0.4.0
|
||||||
[app]
|
app.version.code=4
|
||||||
id=one.tactility.epubreader
|
app.name=Epub Reader
|
||||||
versionName=0.1.0
|
app.description=Epub and text file reader. Requires PSRAM!
|
||||||
versionCode=1
|
|
||||||
name=Epub Reader
|
|
||||||
description=Epub and text file reader. Requires PSRAM!
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
|
|
||||||
|
if (DEFINED ENV{TACTILITY_SDK_PATH})
|
||||||
|
set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH})
|
||||||
|
else()
|
||||||
|
set(TACTILITY_SDK_PATH "../../release/TactilitySDK")
|
||||||
|
message(WARNING "TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake")
|
||||||
|
set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH})
|
||||||
|
|
||||||
|
project(EspNowBridge)
|
||||||
|
tactility_project(EspNowBridge)
|
||||||
Binary file not shown.
@@ -0,0 +1,11 @@
|
|||||||
|
file(GLOB_RECURSE SOURCE_FILES
|
||||||
|
Source/*.c*
|
||||||
|
)
|
||||||
|
|
||||||
|
idf_component_register(
|
||||||
|
SRCS ${SOURCE_FILES}
|
||||||
|
# Library headers must be included directly,
|
||||||
|
# because all regular dependencies get stripped by elf_loader's cmake script
|
||||||
|
INCLUDE_DIRS ../../../Libraries/TactilityCpp/Include
|
||||||
|
REQUIRES TactilitySDK bootloader_support esp_app_format
|
||||||
|
)
|
||||||
@@ -0,0 +1,739 @@
|
|||||||
|
#include "EspNowBridge.h"
|
||||||
|
|
||||||
|
#include <tactility/device.h>
|
||||||
|
#include <tactility/drivers/wifi.h>
|
||||||
|
#include <tactility/wifi_auto_scan.h>
|
||||||
|
#include <tactility/firmware/firmware.h>
|
||||||
|
|
||||||
|
#include <tt_app.h>
|
||||||
|
#include <tt_app_fileselection.h>
|
||||||
|
#include <tt_bundle.h>
|
||||||
|
#include <tt_lock.h>
|
||||||
|
#include <tt_lvgl.h>
|
||||||
|
#include <tt_lvgl_toolbar.h>
|
||||||
|
|
||||||
|
#include <esp_app_desc.h>
|
||||||
|
#include <esp_app_format.h>
|
||||||
|
#include <esp_system.h>
|
||||||
|
|
||||||
|
#include <tactility/log.h>
|
||||||
|
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cinttypes>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
static constexpr auto* TAG = "EspNowBridge";
|
||||||
|
static constexpr size_t CHUNK_SIZE = 1500;
|
||||||
|
static constexpr uint32_t TRANSPORT_WAIT_TIMEOUT_MS = 5000;
|
||||||
|
static constexpr uint32_t UPDATE_TASK_STACK_SIZE = 8192;
|
||||||
|
|
||||||
|
AutoScanPauseGuard::AutoScanPauseGuard() { wifi_auto_scan_set_paused(true); }
|
||||||
|
AutoScanPauseGuard::~AutoScanPauseGuard() { wifi_auto_scan_set_paused(false); }
|
||||||
|
|
||||||
|
// Binary partition table format (gen_esp32part.py STRUCT_FORMAT '<2sBBLL16sL'): a flat array of
|
||||||
|
// 32-byte little-endian records starting at flash offset PARTITION_TABLE_OFFSET, terminated by
|
||||||
|
// an all-0xFF entry or an MD5-checksum record (magic 0xEBEB). Not exposed as a C header by
|
||||||
|
// ESP-IDF (only the Python generator knows the format) - this is a hand-ported minimal reader,
|
||||||
|
// just enough to locate the app partition inside a merged/factory bin.
|
||||||
|
static constexpr size_t PARTITION_TABLE_OFFSET = 0x8000;
|
||||||
|
static constexpr size_t PARTITION_TABLE_MAX_ENTRIES = 128; // covers the largest partition table IDF supports (0x1000 / 32)
|
||||||
|
static constexpr uint16_t PARTITION_ENTRY_MAGIC = 0x50AA; // little-endian bytes 0xAA, 0x50
|
||||||
|
static constexpr uint16_t PARTITION_MD5_MAGIC = 0xEBEB;
|
||||||
|
static constexpr uint8_t PARTITION_TYPE_APP = 0x00;
|
||||||
|
static constexpr uint8_t PARTITION_SUBTYPE_FACTORY = 0x00;
|
||||||
|
static constexpr uint8_t PARTITION_SUBTYPE_OTA_0 = 0x10;
|
||||||
|
|
||||||
|
struct __attribute__((packed)) PartitionEntry {
|
||||||
|
uint16_t magic;
|
||||||
|
uint8_t type;
|
||||||
|
uint8_t subtype;
|
||||||
|
uint32_t offset;
|
||||||
|
uint32_t size;
|
||||||
|
char name[16];
|
||||||
|
uint32_t flags;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(PartitionEntry) == 32, "partition table entry must be 32 bytes");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans the partition table embedded in a merged/factory bin (at PARTITION_TABLE_OFFSET) for
|
||||||
|
* the app partition to flash: prefers "factory" if present, otherwise the first OTA slot
|
||||||
|
* (ota_0) - matches what a real M5Stack ESP-Hosted factory image contains.
|
||||||
|
* @return true if an app partition was found, with appOffset/appSize set to its location
|
||||||
|
* within the file (these are the same as the absolute flash offsets the merged bin preserves).
|
||||||
|
*/
|
||||||
|
static bool findAppPartitionInMergedBin(FILE* file, size_t& appOffset, size_t& appSize) {
|
||||||
|
if (fseek(file, static_cast<long>(PARTITION_TABLE_OFFSET), SEEK_SET) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool foundFactory = false;
|
||||||
|
bool foundOta0 = false;
|
||||||
|
size_t factoryOffset = 0, factorySize = 0;
|
||||||
|
size_t ota0Offset = 0, ota0Size = 0;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < PARTITION_TABLE_MAX_ENTRIES; i++) {
|
||||||
|
PartitionEntry entry;
|
||||||
|
if (fread(&entry, 1, sizeof(entry), file) != sizeof(entry)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (entry.magic == PARTITION_MD5_MAGIC) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (entry.magic != PARTITION_ENTRY_MAGIC) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (entry.type == PARTITION_TYPE_APP) {
|
||||||
|
if (entry.subtype == PARTITION_SUBTYPE_FACTORY) {
|
||||||
|
foundFactory = true;
|
||||||
|
factoryOffset = entry.offset;
|
||||||
|
factorySize = entry.size;
|
||||||
|
} else if (entry.subtype == PARTITION_SUBTYPE_OTA_0 && !foundOta0) {
|
||||||
|
foundOta0 = true;
|
||||||
|
ota0Offset = entry.offset;
|
||||||
|
ota0Size = entry.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundFactory) {
|
||||||
|
appOffset = factoryOffset;
|
||||||
|
appSize = factorySize;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (foundOta0) {
|
||||||
|
appOffset = ota0Offset;
|
||||||
|
appSize = ota0Size;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the app image at the given file offset and extracts its version string. The actual
|
||||||
|
* transfer size used for the OTA loop is just the real remaining file size from appOffset (see
|
||||||
|
* performUpdate) - hand-computing the image's "logical" size from segment headers + checksum/
|
||||||
|
* hash padding drifts a bit short of the real length, so we just use the file size instead.
|
||||||
|
*/
|
||||||
|
static bool parseImageHeader(FILE* file, size_t appOffset, char* versionOut, size_t versionOutLen, std::string* errorOut = nullptr) {
|
||||||
|
esp_image_header_t imageHeader;
|
||||||
|
if (fseek(file, static_cast<long>(appOffset), SEEK_SET) != 0 ||
|
||||||
|
fread(&imageHeader, 1, sizeof(imageHeader), file) != sizeof(imageHeader)) {
|
||||||
|
if (errorOut != nullptr) {
|
||||||
|
*errorOut = "Failed to read image header";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageHeader.magic != ESP_IMAGE_HEADER_MAGIC) {
|
||||||
|
if (errorOut != nullptr) {
|
||||||
|
*errorOut = "Selected file is not a valid firmware image (bad magic)";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail fast on a wrong-chip image (e.g. an ESP32 or S3 binary picked by mistake) before
|
||||||
|
// streaming the whole file over the paced, slow bridge link - esp_hosted_slave_ota_end()
|
||||||
|
// would eventually catch this too, but only after the entire transfer already completed.
|
||||||
|
if (imageHeader.chip_id != ESP_CHIP_ID_ESP32C6) {
|
||||||
|
if (errorOut != nullptr) {
|
||||||
|
char buf[96];
|
||||||
|
snprintf(buf, sizeof(buf), "Wrong chip: image targets chip id %u, expected ESP32-C6",
|
||||||
|
(unsigned)imageHeader.chip_id);
|
||||||
|
*errorOut = buf;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_image_segment_header_t segmentHeader;
|
||||||
|
size_t firstSegmentOffset = appOffset + sizeof(imageHeader);
|
||||||
|
if (fseek(file, static_cast<long>(firstSegmentOffset), SEEK_SET) != 0 ||
|
||||||
|
fread(&segmentHeader, 1, sizeof(segmentHeader), file) != sizeof(segmentHeader)) {
|
||||||
|
if (errorOut != nullptr) {
|
||||||
|
*errorOut = "Failed to read first segment header";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_app_desc_t appDesc;
|
||||||
|
size_t appDescOffset = appOffset + sizeof(imageHeader) + sizeof(segmentHeader);
|
||||||
|
if (fseek(file, static_cast<long>(appDescOffset), SEEK_SET) == 0 && fread(&appDesc, 1, sizeof(appDesc), file) == sizeof(appDesc)) {
|
||||||
|
strncpy(versionOut, appDesc.version, versionOutLen - 1);
|
||||||
|
versionOut[versionOutLen - 1] = '\0';
|
||||||
|
} else {
|
||||||
|
strncpy(versionOut, "unknown", versionOutLen - 1);
|
||||||
|
versionOut[versionOutLen - 1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool getCurrentVersionString(const FirmwareOps* ops, void* ctx, char* versionOut, size_t versionOutLen) {
|
||||||
|
FirmwareInfo info = {};
|
||||||
|
if (ops == nullptr || ops->get_info(ctx, &info) != ERROR_NONE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.name[0] != '\0') {
|
||||||
|
snprintf(versionOut, versionOutLen, "%u.%u.%u (%s)",
|
||||||
|
(unsigned)info.fw_major, (unsigned)info.fw_minor, (unsigned)info.fw_patch, info.name);
|
||||||
|
} else {
|
||||||
|
snprintf(versionOut, versionOutLen, "%u.%u.%u",
|
||||||
|
(unsigned)info.fw_major, (unsigned)info.fw_minor, (unsigned)info.fw_patch);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Only slave firmware >= v2.6.0 implements esp_hosted_slave_ota_activate() - older slaves
|
||||||
|
* reject/lack the RPC entirely. Matches upstream's host_performs_slave_ota example. */
|
||||||
|
static bool activateSupported(uint32_t major, uint32_t minor) {
|
||||||
|
return (major > 2) || (major == 2 && minor > 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::atomic<EspNowBridge*> EspNowBridge::liveInstance_{nullptr};
|
||||||
|
|
||||||
|
void EspNowBridge::onCreate(AppHandle app) {
|
||||||
|
appHandle_ = app;
|
||||||
|
taskDoneSemaphore_ = xSemaphoreCreateBinary();
|
||||||
|
liveInstance_ = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EspNowBridge::onDestroy(AppHandle /*app*/) {
|
||||||
|
// Clear liveInstance_ first so any task still running bails out at its next liveInstance_
|
||||||
|
// check instead of continuing to touch this instance's members.
|
||||||
|
liveInstance_ = nullptr;
|
||||||
|
|
||||||
|
// Wait for any outstanding background task (OTA update, transport-wait) to actually finish -
|
||||||
|
// the app framework frees this instance shortly after onDestroy() returns, so a task that
|
||||||
|
// outlives it would dereference freed memory.
|
||||||
|
while (outstandingTasks_.load() > 0) {
|
||||||
|
if (taskDoneSemaphore_ != nullptr) {
|
||||||
|
xSemaphoreTake(taskDoneSemaphore_, pdMS_TO_TICKS(1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskDoneSemaphore_ != nullptr) {
|
||||||
|
vSemaphoreDelete(taskDoneSemaphore_);
|
||||||
|
taskDoneSemaphore_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EspNowBridge::refreshCurrentVersion() {
|
||||||
|
char versionStr[32];
|
||||||
|
if (getCurrentVersionString(firmwareOps_, firmwareCtx_, versionStr, sizeof(versionStr))) {
|
||||||
|
lv_label_set_text_fmt(currentVersionLabel_, "Co-processor firmware: %s", versionStr);
|
||||||
|
} else {
|
||||||
|
lv_label_set_text(currentVersionLabel_, "Co-processor firmware: unknown (link not up)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EspNowBridge::isWifiRadioOn() {
|
||||||
|
if (wifiDevice_ == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
WifiRadioState radioState = WIFI_RADIO_STATE_OFF;
|
||||||
|
if (wifi_get_radio_state(wifiDevice_, &radioState) != ERROR_NONE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// ON with any station state (disconnected/pending/connected) is fine - the ESP-NOW bridge
|
||||||
|
// just needs the radio + esp_hosted transport up, not a completed AP connection.
|
||||||
|
return radioState == WIFI_RADIO_STATE_ON;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EspNowBridge::refreshWifiPrompt() {
|
||||||
|
if (isWifiRadioOn()) {
|
||||||
|
lv_obj_add_flag(enableWifiButton_, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
setUpdateButtonsDisabled(false);
|
||||||
|
} else {
|
||||||
|
lv_obj_clear_flag(enableWifiButton_, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
setUpdateButtonsDisabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EspNowBridge::setUpdateButtonsDisabled(bool disabled) {
|
||||||
|
if (disabled) {
|
||||||
|
lv_obj_add_state(updateButton_, LV_STATE_DISABLED);
|
||||||
|
lv_obj_add_state(updateBundledButton_, LV_STATE_DISABLED);
|
||||||
|
} else {
|
||||||
|
lv_obj_clear_state(updateButton_, LV_STATE_DISABLED);
|
||||||
|
lv_obj_clear_state(updateBundledButton_, LV_STATE_DISABLED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EspNowBridge::setStatus(const std::string& text) {
|
||||||
|
lv_label_set_text(statusLabel_, text.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void EspNowBridge::setProgress(int percent) {
|
||||||
|
lv_bar_set_value(progressBar_, percent, LV_ANIM_OFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
struct UiDispatchPayload {
|
||||||
|
EspNowBridge* instance;
|
||||||
|
void (*work)(EspNowBridge&, void*);
|
||||||
|
void* context;
|
||||||
|
void (*freeContext)(void*);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void EspNowBridge::dispatchToUi(void (*work)(EspNowBridge&, void*), void* context, void (*freeContext)(void*)) {
|
||||||
|
auto* payload = new UiDispatchPayload{this, work, context, freeContext};
|
||||||
|
// lv_async_call() itself is an LVGL operation and must be lock-guarded when called from a
|
||||||
|
// non-LVGL task (see tt_lvgl_lock()'s doc comment) - the OTA worker task calls dispatchToUi()
|
||||||
|
// repeatedly during the transfer, and without this lock most of those calls were silently
|
||||||
|
// racing LVGL's own task and getting lost (only the very last status update, right before
|
||||||
|
// esp_restart(), happened to land - everything else stayed stuck at "Waiting for
|
||||||
|
// co-processor link...").
|
||||||
|
bool locked = tt_lvgl_lock(TT_LVGL_DEFAULT_LOCK_TIME);
|
||||||
|
if (!locked) {
|
||||||
|
// Without the lock, lv_async_call() itself would be touching LVGL's internal timer list
|
||||||
|
// unguarded - and if it happened to still enqueue successfully, the callback below would
|
||||||
|
// later fire against `payload` after we've already freed it here. Drop the update instead.
|
||||||
|
if (freeContext != nullptr) {
|
||||||
|
freeContext(context);
|
||||||
|
}
|
||||||
|
delete payload;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_result_t result = lv_async_call([](void* userData) {
|
||||||
|
auto* payload = static_cast<UiDispatchPayload*>(userData);
|
||||||
|
if (EspNowBridge::liveInstance_.load() == payload->instance && payload->instance->isShown_.load()) {
|
||||||
|
payload->work(*payload->instance, payload->context);
|
||||||
|
}
|
||||||
|
if (payload->freeContext != nullptr) {
|
||||||
|
payload->freeContext(payload->context);
|
||||||
|
}
|
||||||
|
delete payload;
|
||||||
|
}, payload);
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
|
||||||
|
if (result != LV_RESULT_OK) {
|
||||||
|
if (freeContext != nullptr) {
|
||||||
|
freeContext(context);
|
||||||
|
}
|
||||||
|
delete payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void workSetStatus(EspNowBridge& app, void* context) {
|
||||||
|
app.setStatus(*static_cast<std::string*>(context));
|
||||||
|
}
|
||||||
|
void freeString(void* context) { delete static_cast<std::string*>(context); }
|
||||||
|
|
||||||
|
void workSetProgress(EspNowBridge& app, void* context) {
|
||||||
|
app.setProgress(*static_cast<int*>(context));
|
||||||
|
}
|
||||||
|
void freeInt(void* context) { delete static_cast<int*>(context); }
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void EspNowBridge::performUpdate(const std::string& filePath) {
|
||||||
|
dispatchToUi([](EspNowBridge& app, void*) {
|
||||||
|
app.setUpdateButtonsDisabled(true);
|
||||||
|
app.setProgress(0);
|
||||||
|
app.setStatus("Waiting for co-processor link...");
|
||||||
|
}, nullptr, nullptr);
|
||||||
|
|
||||||
|
if (firmwareOps_ == nullptr) {
|
||||||
|
dispatchToUi([](EspNowBridge& app, void*) {
|
||||||
|
app.setStatus("This WiFi device has no updatable co-processor");
|
||||||
|
app.setUpdateButtonsDisabled(false);
|
||||||
|
}, nullptr, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!firmwareOps_->wait_ready(firmwareCtx_, TRANSPORT_WAIT_TIMEOUT_MS)) {
|
||||||
|
dispatchToUi([](EspNowBridge& app, void*) {
|
||||||
|
app.setStatus("Co-processor link not available - update cancelled");
|
||||||
|
app.setUpdateButtonsDisabled(false);
|
||||||
|
}, nullptr, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FILE* file = fopen(filePath.c_str(), "rb");
|
||||||
|
if (file == nullptr) {
|
||||||
|
dispatchToUi([](EspNowBridge& app, void*) {
|
||||||
|
app.setStatus("Failed to open selected file");
|
||||||
|
app.setUpdateButtonsDisabled(false);
|
||||||
|
}, nullptr, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fseek(file, 0, SEEK_END);
|
||||||
|
long fileSizeSigned = ftell(file);
|
||||||
|
if (fileSizeSigned <= 0) {
|
||||||
|
fclose(file);
|
||||||
|
dispatchToUi([](EspNowBridge& app, void*) {
|
||||||
|
app.setStatus("Failed to determine file size");
|
||||||
|
app.setUpdateButtonsDisabled(false);
|
||||||
|
}, nullptr, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
size_t fileSize = static_cast<size_t>(fileSizeSigned);
|
||||||
|
|
||||||
|
// Support both a plain app image (starting with the app image header at offset 0) and a
|
||||||
|
// merged/factory bin (e.g. M5Stack's official ESP-Hosted factory image) - detected by whether
|
||||||
|
// a valid partition table is found at PARTITION_TABLE_OFFSET.
|
||||||
|
size_t appOffset = 0;
|
||||||
|
size_t partitionSize = 0;
|
||||||
|
bool isMergedBin = findAppPartitionInMergedBin(file, appOffset, partitionSize);
|
||||||
|
if (isMergedBin && appOffset >= fileSize) {
|
||||||
|
fclose(file);
|
||||||
|
dispatchToUi([](EspNowBridge& app, void*) {
|
||||||
|
app.setStatus("Merged bin's app partition is outside the file - selected file looks truncated");
|
||||||
|
app.setUpdateButtonsDisabled(false);
|
||||||
|
}, nullptr, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char newVersion[32];
|
||||||
|
std::string parseError;
|
||||||
|
if (!parseImageHeader(file, appOffset, newVersion, sizeof(newVersion), &parseError)) {
|
||||||
|
fclose(file);
|
||||||
|
dispatchToUi(workSetStatus, new std::string(parseError), freeString);
|
||||||
|
dispatchToUi([](EspNowBridge& app, void*) {
|
||||||
|
app.setUpdateButtonsDisabled(false);
|
||||||
|
}, nullptr, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merged bins pad the app partition to its declared size; a plain app image is exactly as
|
||||||
|
// long as the app itself. Transfer whichever is smaller.
|
||||||
|
size_t remainingInFile = fileSize - appOffset;
|
||||||
|
size_t firmwareSize = isMergedBin ? std::min(partitionSize, remainingInFile) : remainingInFile;
|
||||||
|
|
||||||
|
std::string versionStr(newVersion);
|
||||||
|
{
|
||||||
|
char buf[64];
|
||||||
|
snprintf(buf, sizeof(buf), "Pushing firmware %s...", versionStr.c_str());
|
||||||
|
dispatchToUi(workSetStatus, new std::string(buf), freeString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Held on the app instance (not a local variable) so it outlives this function - see
|
||||||
|
// heldAutoScanPauseGuard_'s declaration for why. Released when the host actually restarts
|
||||||
|
// (moot, since esp_restart() doesn't return) or if the update fails early below.
|
||||||
|
heldAutoScanPauseGuard_.emplace();
|
||||||
|
|
||||||
|
FirmwareUpdateRequest updateRequest = {};
|
||||||
|
updateRequest.image_size = firmwareSize;
|
||||||
|
FirmwareUpdateHandle* handle = nullptr;
|
||||||
|
if (firmwareOps_->begin(firmwareCtx_, &updateRequest, &handle) != ERROR_NONE) {
|
||||||
|
fclose(file);
|
||||||
|
heldAutoScanPauseGuard_.reset();
|
||||||
|
dispatchToUi([](EspNowBridge& app, void*) {
|
||||||
|
app.setStatus("Failed to start OTA on co-processor");
|
||||||
|
app.setUpdateButtonsDisabled(false);
|
||||||
|
}, nullptr, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fseek(file, static_cast<long>(appOffset), SEEK_SET) != 0) {
|
||||||
|
fclose(file);
|
||||||
|
firmwareOps_->abort(handle);
|
||||||
|
heldAutoScanPauseGuard_.reset();
|
||||||
|
dispatchToUi([](EspNowBridge& app, void*) {
|
||||||
|
app.setStatus("Failed to seek to firmware start");
|
||||||
|
app.setUpdateButtonsDisabled(false);
|
||||||
|
}, nullptr, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t chunk[CHUNK_SIZE];
|
||||||
|
size_t sent = 0;
|
||||||
|
bool writeFailed = false;
|
||||||
|
int lastReportedPercent = -1;
|
||||||
|
|
||||||
|
while (sent < firmwareSize) {
|
||||||
|
size_t toRead = (firmwareSize - sent > CHUNK_SIZE) ? CHUNK_SIZE : (firmwareSize - sent);
|
||||||
|
size_t actuallyRead = fread(chunk, 1, toRead, file);
|
||||||
|
if (actuallyRead != toRead) {
|
||||||
|
LOG_E(TAG, "Failed to read file at offset %zu", sent);
|
||||||
|
writeFailed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firmwareOps_->write(handle, chunk, actuallyRead) != ERROR_NONE) {
|
||||||
|
LOG_E(TAG, "firmwareOps_->write() failed at offset %zu", sent);
|
||||||
|
writeFailed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pace the transfer - esp_hosted's SDIO driver only retries a write twice with no
|
||||||
|
// backoff before giving up and restarting the host. Back-to-back chunk writes with zero
|
||||||
|
// gap were observed to saturate the bus enough to trigger a genuine SDIO timeout
|
||||||
|
// mid-transfer, not just around the post-activate reboot.
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(5));
|
||||||
|
|
||||||
|
sent += actuallyRead;
|
||||||
|
|
||||||
|
// Only touch LVGL every couple of percent, not every 1500-byte chunk - frequent
|
||||||
|
// display-bus activity during the transfer was implicated in SDIO transport crashes
|
||||||
|
// under sustained OTA write load.
|
||||||
|
int percent = (int)((sent * 100) / firmwareSize);
|
||||||
|
if (percent != lastReportedPercent) {
|
||||||
|
dispatchToUi(workSetProgress, new int(percent), freeInt);
|
||||||
|
lastReportedPercent = percent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose(file);
|
||||||
|
|
||||||
|
if (writeFailed) {
|
||||||
|
firmwareOps_->abort(handle);
|
||||||
|
heldAutoScanPauseGuard_.reset();
|
||||||
|
dispatchToUi([](EspNowBridge& app, void*) {
|
||||||
|
app.setStatus("Update failed while transferring firmware");
|
||||||
|
app.setUpdateButtonsDisabled(false);
|
||||||
|
}, nullptr, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firmwareOps_->finish(handle) != ERROR_NONE) {
|
||||||
|
heldAutoScanPauseGuard_.reset();
|
||||||
|
dispatchToUi([](EspNowBridge& app, void*) {
|
||||||
|
app.setStatus("Failed to finalize OTA on co-processor");
|
||||||
|
app.setUpdateButtonsDisabled(false);
|
||||||
|
}, nullptr, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the *currently running* (pre-update) slave version - the new image isn't running
|
||||||
|
// yet - and skip straight to the required host restart for older slaves.
|
||||||
|
FirmwareInfo runningInfo = {};
|
||||||
|
bool canActivate = firmwareOps_->get_info(firmwareCtx_, &runningInfo) == ERROR_NONE
|
||||||
|
&& activateSupported(runningInfo.fw_major, runningInfo.fw_minor);
|
||||||
|
|
||||||
|
if (canActivate) {
|
||||||
|
if (firmwareOps_->activate(firmwareCtx_) != ERROR_NONE) {
|
||||||
|
heldAutoScanPauseGuard_.reset();
|
||||||
|
dispatchToUi([](EspNowBridge& app, void*) {
|
||||||
|
app.setStatus("Failed to activate new firmware - co-processor still running old firmware");
|
||||||
|
app.setUpdateButtonsDisabled(false);
|
||||||
|
}, nullptr, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// heldAutoScanPauseGuard_ is deliberately left held (never explicitly released) - the host
|
||||||
|
// restarts itself immediately below, and there's no safe window to resume normal WiFi
|
||||||
|
// activity before that.
|
||||||
|
{
|
||||||
|
char buf[80];
|
||||||
|
if (canActivate) {
|
||||||
|
snprintf(buf, sizeof(buf), "Firmware %s activated - restarting...", versionStr.c_str());
|
||||||
|
} else {
|
||||||
|
snprintf(buf, sizeof(buf), "Firmware %s pushed - restarting to apply...", versionStr.c_str());
|
||||||
|
}
|
||||||
|
dispatchToUi(workSetStatus, new std::string(buf), freeString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give the status message above a moment to actually be seen before the restart cuts the
|
||||||
|
// display, then restart.
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1500));
|
||||||
|
esp_restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EspNowBridge::updateTaskEntry(void* arg) {
|
||||||
|
auto* self = static_cast<EspNowBridge*>(arg);
|
||||||
|
self->performUpdate(self->pendingUpdateFilePath_);
|
||||||
|
self->updateTask_ = nullptr;
|
||||||
|
if (self->outstandingTasks_.fetch_sub(1) == 1 && self->taskDoneSemaphore_ != nullptr) {
|
||||||
|
xSemaphoreGive(self->taskDoneSemaphore_);
|
||||||
|
}
|
||||||
|
vTaskDelete(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EspNowBridge::startUpdateTask(const std::string& filePath) {
|
||||||
|
if (updateTask_ != nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingUpdateFilePath_ = filePath;
|
||||||
|
outstandingTasks_.fetch_add(1);
|
||||||
|
if (xTaskCreate(updateTaskEntry, "espnow_bridge_ota", UPDATE_TASK_STACK_SIZE / sizeof(StackType_t), this, tskIDLE_PRIORITY + 1, &updateTask_) != pdPASS) {
|
||||||
|
outstandingTasks_.fetch_sub(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EspNowBridge::onUpdateButtonClicked(lv_event_t* /*event*/) {
|
||||||
|
auto* self = liveInstance_.load();
|
||||||
|
if (self == nullptr || !self->isWifiRadioOn()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self->pickFileLaunchId_ = tt_app_fileselection_start_for_existing_file();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name of the slave bridge firmware bundled in this app's assets/ folder
|
||||||
|
// lets users flash the known-good bridge firmware without needing to source/copy a
|
||||||
|
// .bin onto the SD card themselves. The SD-card picker (onUpdateButtonClicked above) stays
|
||||||
|
// available too, for factory-image downgrades or custom builds.
|
||||||
|
static constexpr auto* BUNDLED_FIRMWARE_ASSET_NAME = "espnow_bridge_slave_c6.bin";
|
||||||
|
|
||||||
|
void EspNowBridge::onUpdateBundledButtonClicked(lv_event_t* /*event*/) {
|
||||||
|
auto* self = liveInstance_.load();
|
||||||
|
if (self == nullptr || !self->isWifiRadioOn()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
char assetPath[256] = {};
|
||||||
|
size_t assetPathSize = sizeof(assetPath);
|
||||||
|
tt_app_get_assets_child_path(self->appHandle_, BUNDLED_FIRMWARE_ASSET_NAME, assetPath, &assetPathSize);
|
||||||
|
if (assetPath[0] == '\0') {
|
||||||
|
LOG_E(TAG, "Failed to resolve bundled firmware asset path");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self->startUpdateTask(assetPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EspNowBridge::onEnableWifiButtonClicked(lv_event_t* /*event*/) {
|
||||||
|
auto* self = liveInstance_.load();
|
||||||
|
if (self == nullptr || self->wifiDevice_ == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
device_start(self->wifiDevice_);
|
||||||
|
// start_device() allocates a fresh driver context (Platforms/platform-esp32's
|
||||||
|
// esp32_wifi.cpp), which wipes any event callback registered before the device was started -
|
||||||
|
// re-register now that it's actually running. Also refresh once directly rather than relying
|
||||||
|
// solely on the next WifiEvent, so the "WiFi on" prompt updates immediately even though the
|
||||||
|
// co-processor firmware version below isn't available yet.
|
||||||
|
wifi_add_event_callback(self->wifiDevice_, self, onWifiEvent);
|
||||||
|
self->refreshWifiPrompt();
|
||||||
|
self->refreshCurrentVersion();
|
||||||
|
|
||||||
|
// The co-processor RPC transport isn't up the instant device_start() returns - it comes up
|
||||||
|
// asynchronously (~1-2s later) - so firmwareOps_->get_info() above reliably fails right after
|
||||||
|
// enabling WiFi. Nothing else reliably re-triggers a version refresh once the transport
|
||||||
|
// actually comes up (WifiEvent only covers radio/station state, not transport readiness), so
|
||||||
|
// wait for it explicitly on a background task and refresh once it's ready.
|
||||||
|
if (self->firmwareOps_ != nullptr) {
|
||||||
|
self->outstandingTasks_.fetch_add(1);
|
||||||
|
if (xTaskCreate(waitForTransportTaskEntry, "espnow_bridge_wait", 4096 / sizeof(StackType_t), self, tskIDLE_PRIORITY + 1, nullptr) != pdPASS) {
|
||||||
|
self->outstandingTasks_.fetch_sub(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EspNowBridge::waitForTransportTaskEntry(void* arg) {
|
||||||
|
auto* self = static_cast<EspNowBridge*>(arg);
|
||||||
|
constexpr uint32_t WAIT_TIMEOUT_MS = 10000;
|
||||||
|
// liveInstance_ must be checked before touching any member of self - if onDestroy() already
|
||||||
|
// ran, `self` may be freed, and dereferencing self->firmwareOps_ first would be a
|
||||||
|
// use-after-free even just to read the pointer.
|
||||||
|
if (liveInstance_.load() == self && self->firmwareOps_ != nullptr
|
||||||
|
&& self->firmwareOps_->wait_ready(self->firmwareCtx_, WAIT_TIMEOUT_MS)
|
||||||
|
&& liveInstance_.load() == self) {
|
||||||
|
self->dispatchToUi([](EspNowBridge& app, void*) {
|
||||||
|
app.refreshCurrentVersion();
|
||||||
|
}, nullptr, nullptr);
|
||||||
|
}
|
||||||
|
if (self->outstandingTasks_.fetch_sub(1) == 1 && self->taskDoneSemaphore_ != nullptr) {
|
||||||
|
xSemaphoreGive(self->taskDoneSemaphore_);
|
||||||
|
}
|
||||||
|
vTaskDelete(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EspNowBridge::onWifiEvent(Device* /*device*/, void* callbackContext, WifiEvent /*event*/) {
|
||||||
|
auto* self = static_cast<EspNowBridge*>(callbackContext);
|
||||||
|
if (liveInstance_.load() != self) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self->dispatchToUi([](EspNowBridge& app, void*) {
|
||||||
|
app.refreshWifiPrompt();
|
||||||
|
app.refreshCurrentVersion();
|
||||||
|
}, nullptr, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EspNowBridge::onShow(AppHandle app, lv_obj_t* parent) {
|
||||||
|
isShown_ = true;
|
||||||
|
|
||||||
|
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||||
|
|
||||||
|
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, app);
|
||||||
|
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||||
|
|
||||||
|
auto* wrapper = lv_obj_create(parent);
|
||||||
|
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_style_pad_all(wrapper, 8, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||||
|
lv_obj_set_flex_grow(wrapper, 1);
|
||||||
|
|
||||||
|
currentVersionLabel_ = lv_label_create(wrapper);
|
||||||
|
lv_obj_set_style_pad_bottom(currentVersionLabel_, 12, LV_STATE_DEFAULT);
|
||||||
|
|
||||||
|
enableWifiButton_ = lv_button_create(wrapper);
|
||||||
|
lv_obj_add_event_cb(enableWifiButton_, onEnableWifiButtonClicked, LV_EVENT_CLICKED, nullptr);
|
||||||
|
auto* enableWifiButtonLabel = lv_label_create(enableWifiButton_);
|
||||||
|
lv_label_set_text(enableWifiButtonLabel, "Enable WiFi (required for co-processor link)");
|
||||||
|
lv_obj_set_style_pad_bottom(enableWifiButton_, 12, LV_STATE_DEFAULT);
|
||||||
|
|
||||||
|
updateBundledButton_ = lv_button_create(wrapper);
|
||||||
|
lv_obj_add_event_cb(updateBundledButton_, onUpdateBundledButtonClicked, LV_EVENT_CLICKED, nullptr);
|
||||||
|
auto* updateBundledButtonLabel = lv_label_create(updateBundledButton_);
|
||||||
|
lv_label_set_text(updateBundledButtonLabel, "Update to bundled firmware");
|
||||||
|
lv_obj_set_style_pad_bottom(updateBundledButton_, 12, LV_STATE_DEFAULT);
|
||||||
|
|
||||||
|
updateButton_ = lv_button_create(wrapper);
|
||||||
|
lv_obj_add_event_cb(updateButton_, onUpdateButtonClicked, LV_EVENT_CLICKED, nullptr);
|
||||||
|
auto* updateButtonLabel = lv_label_create(updateButton_);
|
||||||
|
lv_label_set_text(updateButtonLabel, "Update from SD card...");
|
||||||
|
lv_obj_set_style_pad_bottom(updateButton_, 12, LV_STATE_DEFAULT);
|
||||||
|
|
||||||
|
progressBar_ = lv_bar_create(wrapper);
|
||||||
|
lv_obj_set_size(progressBar_, LV_PCT(100), LV_PCT(6));
|
||||||
|
lv_bar_set_range(progressBar_, 0, 100);
|
||||||
|
lv_bar_set_value(progressBar_, 0, LV_ANIM_OFF);
|
||||||
|
|
||||||
|
statusLabel_ = lv_label_create(wrapper);
|
||||||
|
lv_label_set_text(statusLabel_, "Ready");
|
||||||
|
|
||||||
|
wifiDevice_ = wifi_find_first_registered_device();
|
||||||
|
if (wifiDevice_ != nullptr) {
|
||||||
|
wifi_add_event_callback(wifiDevice_, this, onWifiEvent);
|
||||||
|
if (wifi_get_firmware_ops(wifiDevice_, &firmwareOps_, &firmwareCtx_) != ERROR_NONE) {
|
||||||
|
firmwareOps_ = nullptr;
|
||||||
|
firmwareCtx_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshCurrentVersion();
|
||||||
|
refreshWifiPrompt();
|
||||||
|
|
||||||
|
// If an SD-card file was picked before this onShow() ran (FileSelection tears down and
|
||||||
|
// rebuilds this app's whole widget tree), perform the update now that widgets are valid
|
||||||
|
// again. The bundled-firmware button doesn't go through this path - it calls
|
||||||
|
// startUpdateTask() directly since there's no separate app launch/result round trip involved.
|
||||||
|
if (!pendingUpdateFilePath_.empty()) {
|
||||||
|
std::string path = std::move(pendingUpdateFilePath_);
|
||||||
|
pendingUpdateFilePath_.clear();
|
||||||
|
startUpdateTask(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EspNowBridge::onHide(AppHandle /*app*/) {
|
||||||
|
isShown_ = false;
|
||||||
|
if (wifiDevice_ != nullptr) {
|
||||||
|
wifi_remove_event_callback(wifiDevice_, onWifiEvent);
|
||||||
|
wifiDevice_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EspNowBridge::onResult(AppHandle /*app*/, void* /*data*/, AppLaunchId launchId, AppResult result, BundleHandle resultData) {
|
||||||
|
if (launchId != pickFileLaunchId_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pickFileLaunchId_ = 0;
|
||||||
|
|
||||||
|
if (result == APP_RESULT_OK && resultData != nullptr) {
|
||||||
|
char pathBuf[256] = {};
|
||||||
|
if (tt_app_fileselection_get_result_path(resultData, pathBuf, sizeof(pathBuf))) {
|
||||||
|
pendingUpdateFilePath_ = pathBuf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <TactilityCpp/App.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
|
#include <lvgl.h>
|
||||||
|
|
||||||
|
#include <tactility/drivers/wifi.h>
|
||||||
|
|
||||||
|
/** RAII guard: pauses WifiService's background auto-connect scan for the guard's lifetime. See
|
||||||
|
* tactility/wifi_auto_scan.h - belt-and-suspenders measure, not sufficient on its own (see the
|
||||||
|
* REBOOT comment in EspNowBridge.cpp). */
|
||||||
|
class AutoScanPauseGuard {
|
||||||
|
public:
|
||||||
|
AutoScanPauseGuard();
|
||||||
|
~AutoScanPauseGuard();
|
||||||
|
AutoScanPauseGuard(const AutoScanPauseGuard&) = delete;
|
||||||
|
AutoScanPauseGuard& operator=(const AutoScanPauseGuard&) = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
class EspNowBridge final : public App {
|
||||||
|
public:
|
||||||
|
EspNowBridge() = default;
|
||||||
|
EspNowBridge(const EspNowBridge&) = delete;
|
||||||
|
EspNowBridge& operator=(const EspNowBridge&) = delete;
|
||||||
|
|
||||||
|
void onCreate(AppHandle app) override;
|
||||||
|
void onDestroy(AppHandle app) override;
|
||||||
|
void onShow(AppHandle app, lv_obj_t* parent) override;
|
||||||
|
void onHide(AppHandle app) override;
|
||||||
|
void onResult(AppHandle app, void* data, AppLaunchId launchId, AppResult result, BundleHandle resultData) override;
|
||||||
|
|
||||||
|
// Public so the free-function dispatchToUi() work callbacks in EspNowBridge.cpp (which run
|
||||||
|
// outside any member-function's lexical scope, unlike the inline lambdas in performUpdate())
|
||||||
|
// can call them.
|
||||||
|
void setStatus(const std::string& text);
|
||||||
|
void setProgress(int percent);
|
||||||
|
|
||||||
|
private:
|
||||||
|
AppHandle appHandle_ = nullptr;
|
||||||
|
AppLaunchId pickFileLaunchId_ = 0;
|
||||||
|
std::string pendingUpdateFilePath_;
|
||||||
|
Device* wifiDevice_ = nullptr;
|
||||||
|
|
||||||
|
// Resolved once in onShow() via wifi_get_firmware_ops() - null on a WiFi device with no
|
||||||
|
// updatable co-processor (e.g. a native, non-hosted chip). All OTA/version-query calls go
|
||||||
|
// through this generic interface, not any esp_hosted-specific API directly.
|
||||||
|
const FirmwareOps* firmwareOps_ = nullptr;
|
||||||
|
void* firmwareCtx_ = nullptr;
|
||||||
|
|
||||||
|
// Set once in onShow(), false once onHide() tears the widget tree down - checked (via
|
||||||
|
// dispatchToUi(), below) before touching any lv_obj_t*, since the OTA worker task and the
|
||||||
|
// WiFi-event callback can both outlive a hide/app-switch.
|
||||||
|
std::atomic<bool> isShown_{false};
|
||||||
|
|
||||||
|
// Only one EspNowBridge instance is ever live at a time (app loader owns a single instance
|
||||||
|
// per running app), so a single static "is this instance still current" pointer, guarded by
|
||||||
|
// an atomic, substitutes for the internal app's shared_ptr-based lifetime guard - the OTA
|
||||||
|
// worker task and dispatchToUi()'s lv_async_call closures check liveInstance_ == this before
|
||||||
|
// touching any member, instead of holding a shared_ptr to keep `this` alive.
|
||||||
|
static std::atomic<EspNowBridge*> liveInstance_;
|
||||||
|
|
||||||
|
TaskHandle_t updateTask_ = nullptr;
|
||||||
|
|
||||||
|
// Number of background tasks (updateTaskEntry, waitForTransportTaskEntry) currently running
|
||||||
|
// against this instance's members. onDestroy() must wait for this to hit 0 before returning -
|
||||||
|
// the app framework frees this instance shortly after onDestroy() returns (see Loader.cpp),
|
||||||
|
// so any task still touching `this` past that point is a use-after-free.
|
||||||
|
std::atomic<int> outstandingTasks_{0};
|
||||||
|
SemaphoreHandle_t taskDoneSemaphore_ = nullptr;
|
||||||
|
|
||||||
|
// Outlives performUpdate() deliberately, so auto-scan stays paused across the async gap
|
||||||
|
// between performUpdate() returning and the automatic restart - see performUpdate().
|
||||||
|
std::optional<AutoScanPauseGuard> heldAutoScanPauseGuard_;
|
||||||
|
|
||||||
|
lv_obj_t* currentVersionLabel_ = nullptr;
|
||||||
|
lv_obj_t* statusLabel_ = nullptr;
|
||||||
|
lv_obj_t* progressBar_ = nullptr;
|
||||||
|
lv_obj_t* updateButton_ = nullptr;
|
||||||
|
lv_obj_t* updateBundledButton_ = nullptr;
|
||||||
|
lv_obj_t* enableWifiButton_ = nullptr;
|
||||||
|
|
||||||
|
void refreshCurrentVersion();
|
||||||
|
bool isWifiRadioOn();
|
||||||
|
void refreshWifiPrompt();
|
||||||
|
/** Enables/disables both update-trigger buttons together - only one performUpdate() can run
|
||||||
|
* at a time (see updateTask_), regardless of which button started it. */
|
||||||
|
void setUpdateButtonsDisabled(bool disabled);
|
||||||
|
/** Marshal a UI-touching closure onto the LVGL task. Only ever invoked if liveInstance_ is
|
||||||
|
* still this instance (checked at dispatch time and again right before running, on the LVGL
|
||||||
|
* task) and isShown_ is true (this app's widget tree exists). */
|
||||||
|
void dispatchToUi(void (*work)(EspNowBridge&, void*), void* context, void (*freeContext)(void*));
|
||||||
|
void performUpdate(const std::string& filePath);
|
||||||
|
void startUpdateTask(const std::string& filePath);
|
||||||
|
|
||||||
|
static void updateTaskEntry(void* arg);
|
||||||
|
static void onUpdateButtonClicked(lv_event_t* event);
|
||||||
|
static void onUpdateBundledButtonClicked(lv_event_t* event);
|
||||||
|
static void onEnableWifiButtonClicked(lv_event_t* event);
|
||||||
|
static void onWifiEvent(Device* device, void* callbackContext, WifiEvent event);
|
||||||
|
static void waitForTransportTaskEntry(void* arg);
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#include "EspNowBridge.h"
|
||||||
|
#include <TactilityCpp/App.h>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
registerApp<EspNowBridge>();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
manifest.version=0.2
|
||||||
|
target.sdk=0.8.0-dev
|
||||||
|
target.platforms=esp32p4
|
||||||
|
app.id=one.tactility.espnowbridge
|
||||||
|
app.version.name=0.1.0
|
||||||
|
app.version.code=1
|
||||||
|
app.name=ESP-NOW Bridge
|
||||||
|
app.description=Companion app for updating P4 device C6 co-processor firmware to enable ESP-NOW bridge support.
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
[manifest]
|
manifest.version=0.2
|
||||||
version=0.1
|
target.sdk=0.8.0-dev
|
||||||
[target]
|
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
sdk=0.7.0-dev
|
app.id=one.tactility.gpio
|
||||||
platforms=esp32,esp32s3,esp32c6,esp32p4
|
app.version.name=0.7.0
|
||||||
[app]
|
app.version.code=7
|
||||||
id=one.tactility.gpio
|
app.name=GPIO
|
||||||
versionName=0.4.0
|
|
||||||
versionCode=4
|
|
||||||
name=GPIO
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
|
|
||||||
|
if (DEFINED ENV{TACTILITY_SDK_PATH})
|
||||||
|
set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH})
|
||||||
|
else()
|
||||||
|
set(TACTILITY_SDK_PATH "../../release/TactilitySDK")
|
||||||
|
message(WARNING "⚠️ TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake")
|
||||||
|
set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH})
|
||||||
|
|
||||||
|
project(GameBoy)
|
||||||
|
tactility_project(GameBoy)
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# GameBoy Emulator (Tactility Prototype, No Audio)
|
||||||
|
|
||||||
|
DMG Game Boy emulator for Tactility side-loaded apps, using **Peanut-GB** (MIT) as CPU/LCD core.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- Prototype v0.1.0-dev – compiling draft focused on architecture, not yet production-hardened.
|
||||||
|
- **No audio** (ENABLE_SOUND 0). Audio path stubbed for future MiniGB APU or i2s-driven implementation.
|
||||||
|
- Supports MBC1/MBC2/MBC3/MBC5 via Peanut-GB.
|
||||||
|
- ROM loader from SD card.
|
||||||
|
- Save RAM persistence via `.sav` file next to ROM.
|
||||||
|
- LVGL framebuffer: native 160x144 RGB565, integer-scaled if display large enough, centered on black background.
|
||||||
|
- Input: on-screen D-pad + A/B + Start/Select + hardware keyboard arrows + LVGL key events (z= A, x= B).
|
||||||
|
- Timer-driven at ~16ms (~60Hz) calling `gb_run_frame()`.
|
||||||
|
|
||||||
|
## ROM Location
|
||||||
|
|
||||||
|
- Scanned directory: `/sdcard/roms/gb/` for `*.gb`, `*.gbc`, `*.bin` (up to 64 entries).
|
||||||
|
- Default quick-load: `/sdcard/roms/gb/default.gb` – if present on app show, autoloads and jumps directly to emulation.
|
||||||
|
- Place your legally dumped ROMs there; no ROMs are bundled.
|
||||||
|
|
||||||
|
## Save RAM Path Design (stubbed + implemented minimal)
|
||||||
|
|
||||||
|
- Save file = ROM path with extension replaced by `.sav` (e.g. `/sdcard/roms/gb/tetris.gb` -> `/sdcard/roms/gb/tetris.sav`).
|
||||||
|
- Loaded on ROM load, saved on:
|
||||||
|
- switching back to menu
|
||||||
|
- app hide
|
||||||
|
- error recovery path
|
||||||
|
- Size queried via `gb_get_save_size_s()` / `gb_get_save_size()`.
|
||||||
|
- Future improvement: also mirror to app user-data dir (`tt_app_get_user_data_child_path`) if SD is read-only.
|
||||||
|
|
||||||
|
## Memory Considerations (ESP32-S3 / PSRAM)
|
||||||
|
|
||||||
|
- Large buffers allocated via `heap_caps_malloc(..., MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)` with fallback to internal.
|
||||||
|
- ROM buffer: up to 2MB (MBC5 max-ish) – PSRAM preferred.
|
||||||
|
- Cart RAM: variable, often 8KB-32KB, PSRAM.
|
||||||
|
- Framebuffer: 160*144*2 = 46,080 bytes (~45KB) native RGB565. PSRAM preferred. No double buffering needed (line callback writes directly).
|
||||||
|
- No huge heap allocations.
|
||||||
|
- Emulator context `struct gb_s` is static inside AppCtx (~few KB).
|
||||||
|
|
||||||
|
## Controls / Input Mapping
|
||||||
|
|
||||||
|
| GB | On-screen | Keyboard | Remarks |
|
||||||
|
|----|-----------|----------|---------|
|
||||||
|
| D-pad | 4 arrow buttons | LV_KEY_ arrows | press/release tracked |
|
||||||
|
| A | A button (right cluster) | z | |
|
||||||
|
| B | B button (right cluster) | x | |
|
||||||
|
| Start | Sta | Enter / Space | |
|
||||||
|
| Select | Sel | Esc / Backspace | |
|
||||||
|
| Touch | Quadrants not yet separated – buttons cover |
|
||||||
|
|
||||||
|
Future: touch quadrants mapping via `pointToQuadrant` like GameKitInput.
|
||||||
|
|
||||||
|
## LCD Rendering
|
||||||
|
|
||||||
|
- Peanut-GB calls `lcd_draw_line(gb, pixels[160], line)` per scanline.
|
||||||
|
- `pixels` low 2 bits = shade 0-3.
|
||||||
|
- Mapped to olive/gray palette RGB565 (editable).
|
||||||
|
- Canvas buffer is the framebuffer itself.
|
||||||
|
- Scale: if display resolution >= 320x432 => 2x, >=480x576 => 3x via LVGL transform scale (keeps native buffer).
|
||||||
|
|
||||||
|
## No-Audio Limitation
|
||||||
|
|
||||||
|
- `ENABLE_SOUND 0` – audio callbacks not compiled.
|
||||||
|
- To add audio:
|
||||||
|
1. Vendor MiniGB APU (`minigb_apu`) or similar.
|
||||||
|
2. Implement `audio_read` / `audio_write` forwarding to APU.
|
||||||
|
3. Define ENABLE_SOUND 1, include APU, create audio task similar to BookPlayer (i2s_controller).
|
||||||
|
4. Feed APU samples in timer / separate task.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Same as other Tactility apps:
|
||||||
|
|
||||||
|
```
|
||||||
|
. $IDF_PATH/export.sh
|
||||||
|
export TACTILITY_SDK_PATH=...
|
||||||
|
python3 tactility.py Apps/GameBoy build esp32s3 --local-sdk
|
||||||
|
```
|
||||||
|
|
||||||
|
## Licensing
|
||||||
|
|
||||||
|
- App code: GPLv3 (same as Tactility Apps).
|
||||||
|
- Peanut-GB vendored lib: MIT (Copyright (c) 2018-2023 Mahyar Koshkouei). License preserved in `Libraries/PeanutGB/peanut_gb.h`.
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [ ] Improve input: add touch quadrant → D-pad, repeat timers for held buttons.
|
||||||
|
- [ ] Add pause/resume UI, FPS display.
|
||||||
|
- [ ] Add palette selector (auto_assign_palette logic from peanut_sdl).
|
||||||
|
- [ ] Add file picker dialog (`tt_app_selectiondialog_start`) improvement + recursive folder browsing.
|
||||||
|
- [ ] Audio: vendoring `minigb_apu` and creating I2S task.
|
||||||
|
- [ ] Save state beyond cart RAM (full emu snapshot).
|
||||||
|
- [ ] RTC persistence for MBC3 RTC games.
|
||||||
|
- [ ] Error dialog via `tt_app_alertdialog_start`.
|
||||||
|
- [ ] Validate with Cppcheck / clang-format and ESP-IDF build.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
file(GLOB_RECURSE SOURCE_FILES Source/*.c)
|
||||||
|
|
||||||
|
idf_component_register(
|
||||||
|
SRCS ${SOURCE_FILES}
|
||||||
|
INCLUDE_DIRS Source ../../../Libraries/PeanutGB
|
||||||
|
REQUIRES TactilitySDK
|
||||||
|
)
|
||||||
@@ -0,0 +1,679 @@
|
|||||||
|
/**
|
||||||
|
* @file main.c
|
||||||
|
* @brief GameBoy DMG Emulator for Tactility (Peanut-GB prototype, no audio)
|
||||||
|
*
|
||||||
|
* MIT licensed Peanut-GB core vendored in Libraries/PeanutGB/peanut_gb.h
|
||||||
|
* See app README for notes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <tt_app.h>
|
||||||
|
#include <tt_lvgl.h>
|
||||||
|
#include <tt_lvgl_toolbar.h>
|
||||||
|
#include <tt_app_alertdialog.h>
|
||||||
|
#include <tt_lvgl_keyboard.h>
|
||||||
|
|
||||||
|
#include <lvgl.h>
|
||||||
|
/* lv_image_cache_drop is not in public LVGL headers but exported by Tactility firmware */
|
||||||
|
void lv_image_cache_drop(const void * src);
|
||||||
|
#include <esp_log.h>
|
||||||
|
|
||||||
|
/* Board firmware 0.8.0-dev/IDF 5.3.2 does not export esp_log for side-loaded ELFs. */
|
||||||
|
#undef ESP_LOGI
|
||||||
|
#undef ESP_LOGW
|
||||||
|
#undef ESP_LOGE
|
||||||
|
#define ESP_LOGI(tag, fmt, ...) do { (void)(tag); } while (0)
|
||||||
|
#define ESP_LOGW(tag, fmt, ...) do { (void)(tag); } while (0)
|
||||||
|
#define ESP_LOGE(tag, fmt, ...) do { (void)(tag); } while (0)
|
||||||
|
#include <esp_heap_caps.h>
|
||||||
|
#include <esp_timer.h>
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <strings.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#define ENABLE_SOUND 0
|
||||||
|
#define ENABLE_LCD 1
|
||||||
|
#include "peanut_gb.h"
|
||||||
|
|
||||||
|
#define TAG "GameBoy"
|
||||||
|
#define DEFAULT_ROM_PATH "/data/roms/gb/default.gb"
|
||||||
|
#define ROMS_DIR "/data/roms/gb"
|
||||||
|
#define MAX_ROMS 64
|
||||||
|
#define MAX_PATH 512
|
||||||
|
#define MAX_ROM_SIZE (2 * 1024 * 1024)
|
||||||
|
#define FRAME_W 160
|
||||||
|
#define FRAME_H 144
|
||||||
|
#define TICK_MS 16
|
||||||
|
|
||||||
|
/* RGB565 direct – no bitfield endian ambiguity */
|
||||||
|
static const uint16_t GB_PALETTE[4] = {
|
||||||
|
0xFFFF, // white
|
||||||
|
0x8C51, // light gray ~ 0b10001 100010 10001 but pre-tuned
|
||||||
|
0x4A49, // dark gray
|
||||||
|
0x0000 // black
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef enum { APP_MODE_BROWSER, APP_MODE_EMU } AppMode;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char filename[MAX_PATH];
|
||||||
|
char fullpath[MAX_PATH];
|
||||||
|
} RomEntry;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
struct gb_s gb;
|
||||||
|
uint8_t* rom_data;
|
||||||
|
size_t rom_size;
|
||||||
|
uint8_t* cart_ram;
|
||||||
|
size_t cart_ram_size;
|
||||||
|
char rom_path[MAX_PATH];
|
||||||
|
char rom_title[32];
|
||||||
|
uint8_t joypad_state;
|
||||||
|
|
||||||
|
uint16_t* fb_native; /* RGB565 tightly packed 160*144, raw u16 avoids lv_color16_t bitfield */
|
||||||
|
lv_draw_buf_t* fb_draw_buf; /* draw_buf that canvas src points to – owned by us so we can invalidate cache */
|
||||||
|
lv_obj_t* canvas;
|
||||||
|
lv_obj_t* root_wrapper;
|
||||||
|
lv_obj_t* browser_wrapper;
|
||||||
|
lv_obj_t* emu_wrapper;
|
||||||
|
lv_obj_t* toolbar;
|
||||||
|
lv_obj_t* status_label;
|
||||||
|
lv_obj_t* rom_list;
|
||||||
|
lv_obj_t* controls_cont;
|
||||||
|
lv_timer_t* emu_timer;
|
||||||
|
|
||||||
|
RomEntry roms[MAX_ROMS];
|
||||||
|
int rom_count;
|
||||||
|
int selected_rom_idx;
|
||||||
|
|
||||||
|
lv_obj_t* btn_up;
|
||||||
|
lv_obj_t* btn_down;
|
||||||
|
lv_obj_t* btn_left;
|
||||||
|
lv_obj_t* btn_right;
|
||||||
|
lv_obj_t* btn_a;
|
||||||
|
lv_obj_t* btn_b;
|
||||||
|
lv_obj_t* btn_start;
|
||||||
|
lv_obj_t* btn_select;
|
||||||
|
|
||||||
|
AppHandle app_handle;
|
||||||
|
AppMode mode;
|
||||||
|
bool emu_running;
|
||||||
|
bool framebuffer_allocated;
|
||||||
|
bool rom_loaded;
|
||||||
|
uint32_t fps_frames;
|
||||||
|
int64_t fps_last_us;
|
||||||
|
uint32_t lines_drawn; /* debug: should be 144 per frame */
|
||||||
|
} AppCtx;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
AppCtx* ctx;
|
||||||
|
uint8_t joypad_bit;
|
||||||
|
} BtnUserData;
|
||||||
|
|
||||||
|
/* PSRAM helpers */
|
||||||
|
static void* alloc_psram(size_t size) {
|
||||||
|
void* p = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||||
|
if (!p) p = heap_caps_malloc(size, MALLOC_CAP_8BIT);
|
||||||
|
if (!p) p = malloc(size);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Peanut-GB callbacks */
|
||||||
|
static uint8_t gb_rom_read(struct gb_s* gb, const uint_fast32_t addr) {
|
||||||
|
AppCtx* ctx = (AppCtx*)gb->direct.priv;
|
||||||
|
if (!ctx || !ctx->rom_data) return 0xFF;
|
||||||
|
if (addr < ctx->rom_size) return ctx->rom_data[addr];
|
||||||
|
return 0xFF;
|
||||||
|
}
|
||||||
|
static uint8_t gb_cart_ram_read(struct gb_s* gb, const uint_fast32_t addr) {
|
||||||
|
AppCtx* ctx = (AppCtx*)gb->direct.priv;
|
||||||
|
if (!ctx || !ctx->cart_ram) return 0xFF;
|
||||||
|
if (addr < ctx->cart_ram_size) return ctx->cart_ram[addr];
|
||||||
|
return 0xFF;
|
||||||
|
}
|
||||||
|
static void gb_cart_ram_write(struct gb_s* gb, const uint_fast32_t addr, const uint8_t val) {
|
||||||
|
AppCtx* ctx = (AppCtx*)gb->direct.priv;
|
||||||
|
if (!ctx || !ctx->cart_ram) return;
|
||||||
|
if (addr < ctx->cart_ram_size) ctx->cart_ram[addr] = val;
|
||||||
|
}
|
||||||
|
static void gb_error_handler(struct gb_s* gb, const enum gb_error_e err, const uint16_t addr) {
|
||||||
|
const char* err_str = "UNKNOWN";
|
||||||
|
switch (err) {
|
||||||
|
case GB_INVALID_OPCODE: err_str = "INVALID OPCODE"; break;
|
||||||
|
case GB_INVALID_READ: err_str = "INVALID READ"; break;
|
||||||
|
case GB_INVALID_WRITE: err_str = "INVALID WRITE"; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
ESP_LOGE(TAG, "GB error %s (%d) @ %04X", err_str, err, addr);
|
||||||
|
AppCtx* ctx = (AppCtx*)gb->direct.priv;
|
||||||
|
if (ctx && ctx->cart_ram_size) {
|
||||||
|
char sp[MAX_PATH];
|
||||||
|
strncpy(sp, ctx->rom_path, MAX_PATH-1); sp[MAX_PATH-1]='\0';
|
||||||
|
char* dot=strrchr(sp,'.'); char* sl=strrchr(sp,'/');
|
||||||
|
if (dot && (!sl || dot>sl)) snprintf(dot, MAX_PATH-(dot-sp), ".sav"); else strncat(sp,".sav",MAX_PATH-strlen(sp)-1);
|
||||||
|
FILE* f=fopen(sp,"wb"); if(f){fwrite(ctx->cart_ram,1,ctx->cart_ram_size,f); fclose(f);}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void lcd_draw_line(struct gb_s* gb, const uint8_t* pixels, const uint_fast8_t line) {
|
||||||
|
AppCtx* ctx = (AppCtx*)gb->direct.priv;
|
||||||
|
if (!ctx || !ctx->fb_native) return;
|
||||||
|
if (line >= FRAME_H) return;
|
||||||
|
uint16_t* dst = &ctx->fb_native[line * FRAME_W];
|
||||||
|
for (int x=0;x<FRAME_W;x++) {
|
||||||
|
uint8_t shade = pixels[x] & 0x03;
|
||||||
|
dst[x] = GB_PALETTE[shade];
|
||||||
|
}
|
||||||
|
ctx->lines_drawn++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Save path */
|
||||||
|
static void get_save_path(const char* rom_path, char* out, size_t out_len) {
|
||||||
|
if (!rom_path || !out) return;
|
||||||
|
strncpy(out, rom_path, out_len-1); out[out_len-1]='\0';
|
||||||
|
char* dot=strrchr(out,'.'); char* sl=strrchr(out,'/');
|
||||||
|
if (dot && (!sl || dot>sl)) { snprintf(dot, out_len-(dot-out), ".sav"); }
|
||||||
|
else { strncat(out,".sav",out_len-strlen(out)-1); }
|
||||||
|
}
|
||||||
|
static void load_cart_ram(AppCtx* ctx) {
|
||||||
|
if (!ctx || !ctx->rom_path[0]) return;
|
||||||
|
char save_path[MAX_PATH];
|
||||||
|
get_save_path(ctx->rom_path, save_path, sizeof(save_path));
|
||||||
|
size_t save_sz=0;
|
||||||
|
if (gb_get_save_size_s(&ctx->gb, &save_sz)!=0) save_sz=gb_get_save_size(&ctx->gb);
|
||||||
|
if (save_sz==0) { ESP_LOGI(TAG,"No save RAM"); return; }
|
||||||
|
if (ctx->cart_ram) { heap_caps_free(ctx->cart_ram); ctx->cart_ram=NULL; }
|
||||||
|
ctx->cart_ram_size=save_sz;
|
||||||
|
ctx->cart_ram=alloc_psram(save_sz);
|
||||||
|
if (!ctx->cart_ram){ ESP_LOGE(TAG,"cart RAM alloc fail %zu",save_sz); ctx->cart_ram_size=0; return; }
|
||||||
|
memset(ctx->cart_ram,0,save_sz);
|
||||||
|
FILE* f=fopen(save_path,"rb");
|
||||||
|
if (!f){ ESP_LOGI(TAG,"No save %s",save_path); return; }
|
||||||
|
size_t r=fread(ctx->cart_ram,1,save_sz,f); fclose(f);
|
||||||
|
ESP_LOGI(TAG,"Loaded save %s %zu/%zu",save_path,r,save_sz);
|
||||||
|
}
|
||||||
|
static void save_cart_ram(AppCtx* ctx) {
|
||||||
|
if (!ctx || !ctx->cart_ram || ctx->cart_ram_size==0) return;
|
||||||
|
if (!ctx->rom_path[0]) return;
|
||||||
|
char save_path[MAX_PATH];
|
||||||
|
get_save_path(ctx->rom_path, save_path, sizeof(save_path));
|
||||||
|
FILE* f=fopen(save_path,"wb");
|
||||||
|
if (!f){ ESP_LOGE(TAG,"Save open fail %s",save_path); return; }
|
||||||
|
size_t w=fwrite(ctx->cart_ram,1,ctx->cart_ram_size,f); fclose(f);
|
||||||
|
ESP_LOGI(TAG,"Saved RAM %s %zu",save_path,w);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ROM loading */
|
||||||
|
static bool load_rom_file(AppCtx* ctx, const char* path) {
|
||||||
|
if (!ctx || !path) return false;
|
||||||
|
ESP_LOGI(TAG,"Loading ROM %s",path);
|
||||||
|
FILE* f=fopen(path,"rb");
|
||||||
|
if (!f){ ESP_LOGE(TAG,"Open fail %s",path); return false; }
|
||||||
|
fseek(f,0,SEEK_END); long sz=ftell(f); fseek(f,0,SEEK_SET);
|
||||||
|
if (sz<=0 || sz>MAX_ROM_SIZE){ ESP_LOGE(TAG,"Bad size %ld %s",sz,path); fclose(f); return false; }
|
||||||
|
uint8_t* buf=alloc_psram((size_t)sz);
|
||||||
|
if (!buf){ ESP_LOGE(TAG,"Alloc fail %ld",sz); fclose(f); return false; }
|
||||||
|
size_t read=fread(buf,1,(size_t)sz,f); fclose(f);
|
||||||
|
if (read!=(size_t)sz){ ESP_LOGE(TAG,"Short read %zu vs %ld",read,sz); heap_caps_free(buf); return false; }
|
||||||
|
|
||||||
|
if (ctx->rom_data) { if (ctx->cart_ram) save_cart_ram(ctx); heap_caps_free(ctx->rom_data); }
|
||||||
|
if (ctx->cart_ram){ heap_caps_free(ctx->cart_ram); ctx->cart_ram=NULL; ctx->cart_ram_size=0; }
|
||||||
|
|
||||||
|
ctx->rom_data=buf; ctx->rom_size=(size_t)sz;
|
||||||
|
strncpy(ctx->rom_path,path,sizeof(ctx->rom_path)-1); ctx->rom_path[sizeof(ctx->rom_path)-1]='\0';
|
||||||
|
memset(&ctx->gb,0,sizeof(ctx->gb));
|
||||||
|
ctx->joypad_state=0xFF;
|
||||||
|
enum gb_init_error_e err=gb_init(&ctx->gb, gb_rom_read, gb_cart_ram_read, gb_cart_ram_write, gb_error_handler, ctx);
|
||||||
|
if (err!=GB_INIT_NO_ERROR){ ESP_LOGE(TAG,"gb_init %d",err); heap_caps_free(ctx->rom_data); ctx->rom_data=NULL; ctx->rom_size=0; return false; }
|
||||||
|
gb_init_lcd(&ctx->gb, lcd_draw_line);
|
||||||
|
load_cart_ram(ctx);
|
||||||
|
gb_reset(&ctx->gb);
|
||||||
|
char title[32]={0}; gb_get_rom_name(&ctx->gb, title); strncpy(ctx->rom_title,title,sizeof(ctx->rom_title)-1);
|
||||||
|
ESP_LOGI(TAG,"ROM OK title='%s' size=%zu save=%zu",ctx->rom_title,ctx->rom_size,ctx->cart_ram_size);
|
||||||
|
ctx->rom_loaded=true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static void scan_rom_dir(AppCtx* ctx) {
|
||||||
|
ctx->rom_count=0;
|
||||||
|
DIR* dir=opendir(ROMS_DIR);
|
||||||
|
if (!dir){ ESP_LOGW(TAG,"ROMS dir missing %s",ROMS_DIR); return; }
|
||||||
|
struct dirent* ent;
|
||||||
|
while ((ent=readdir(dir))!=NULL && ctx->rom_count<MAX_ROMS){
|
||||||
|
if (ent->d_name[0]=='.') continue;
|
||||||
|
size_t len=strlen(ent->d_name);
|
||||||
|
if (len<3) continue;
|
||||||
|
bool is_gb = (strcasecmp(ent->d_name+len-3,".gb")==0) || (len>=4 && (strcasecmp(ent->d_name+len-4,".gbc")==0 || strcasecmp(ent->d_name+len-4,".bin")==0));
|
||||||
|
if (!is_gb) continue;
|
||||||
|
RomEntry* e=&ctx->roms[ctx->rom_count++];
|
||||||
|
strncpy(e->filename, ent->d_name, sizeof(e->filename)-1); e->filename[sizeof(e->filename)-1]='\0';
|
||||||
|
snprintf(e->fullpath, sizeof(e->fullpath), "%s/%s", ROMS_DIR, ent->d_name);
|
||||||
|
}
|
||||||
|
closedir(dir);
|
||||||
|
ESP_LOGI(TAG,"Found %d ROMs",ctx->rom_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Emu timer – THIS IS WHERE "FPS but no image" WAS: missing cache drop */
|
||||||
|
static void emu_timer_cb(lv_timer_t* timer) {
|
||||||
|
AppCtx* ctx=(AppCtx*)lv_timer_get_user_data(timer);
|
||||||
|
if (!ctx || !ctx->emu_running || !ctx->rom_loaded || !ctx->canvas) return;
|
||||||
|
ctx->lines_drawn = 0;
|
||||||
|
ctx->gb.direct.joypad = ctx->joypad_state;
|
||||||
|
gb_run_frame(&ctx->gb);
|
||||||
|
|
||||||
|
/* Critical fix: raw buffer mutated, LVGL image cache is stale.
|
||||||
|
Without this, canvas shows whatever was first uploaded (gray/black) and FPS label keeps updating,
|
||||||
|
giving "FPS but no game". */
|
||||||
|
if (ctx->fb_draw_buf) {
|
||||||
|
/* Invalidate both D-Cache (PSRAM) and LVGL image cache */
|
||||||
|
lv_draw_buf_invalidate_cache(ctx->fb_draw_buf, NULL);
|
||||||
|
lv_image_cache_drop(ctx->fb_draw_buf);
|
||||||
|
/* Also invalidate area via the canvas src buf if different object */
|
||||||
|
if (ctx->canvas) {
|
||||||
|
lv_draw_buf_t* c_db = lv_canvas_get_draw_buf(ctx->canvas);
|
||||||
|
if (c_db && c_db != ctx->fb_draw_buf) {
|
||||||
|
lv_draw_buf_invalidate_cache(c_db, NULL);
|
||||||
|
lv_image_cache_drop(c_db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lv_obj_invalidate(ctx->canvas);
|
||||||
|
|
||||||
|
ctx->fps_frames++;
|
||||||
|
int64_t now = esp_timer_get_time();
|
||||||
|
if (ctx->fps_last_us == 0) ctx->fps_last_us = now;
|
||||||
|
int64_t elapsed = now - ctx->fps_last_us;
|
||||||
|
if (elapsed >= 1000000) {
|
||||||
|
uint32_t fps = (uint32_t)((ctx->fps_frames * 1000000ULL) / (uint64_t)elapsed);
|
||||||
|
if (ctx->status_label) {
|
||||||
|
lv_label_set_text_fmt(ctx->status_label, "GB: %s FPS:%lu L:%lu", ctx->rom_title[0] ? ctx->rom_title : "GameBoy", (unsigned long)fps, (unsigned long)ctx->lines_drawn);
|
||||||
|
}
|
||||||
|
printf("GAMEBOY_FPS %lu LINES %lu\n", (unsigned long)fps, (unsigned long)ctx->lines_drawn);
|
||||||
|
ctx->fps_frames = 0;
|
||||||
|
ctx->fps_last_us = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input helpers */
|
||||||
|
static void set_joypad_bit(AppCtx* ctx, uint8_t bit, bool pressed) {
|
||||||
|
if (!ctx) return;
|
||||||
|
if (pressed) ctx->joypad_state &= (uint8_t)~bit;
|
||||||
|
else ctx->joypad_state |= bit;
|
||||||
|
}
|
||||||
|
static void input_down_cb(lv_event_t* e){
|
||||||
|
BtnUserData* ud=(BtnUserData*)lv_event_get_user_data(e);
|
||||||
|
if (!ud) return;
|
||||||
|
set_joypad_bit(ud->ctx, ud->joypad_bit, true);
|
||||||
|
}
|
||||||
|
static void input_up_cb(lv_event_t* e){
|
||||||
|
BtnUserData* ud=(BtnUserData*)lv_event_get_user_data(e);
|
||||||
|
if (!ud) return;
|
||||||
|
set_joypad_bit(ud->ctx, ud->joypad_bit, false);
|
||||||
|
}
|
||||||
|
static void key_press_cb(lv_event_t* e){
|
||||||
|
AppCtx* ctx=(AppCtx*)lv_event_get_user_data(e);
|
||||||
|
uint32_t key=lv_event_get_key(e);
|
||||||
|
switch(key){
|
||||||
|
case LV_KEY_UP: set_joypad_bit(ctx, JOYPAD_UP, true); break;
|
||||||
|
case LV_KEY_DOWN: set_joypad_bit(ctx, JOYPAD_DOWN, true); break;
|
||||||
|
case LV_KEY_LEFT: set_joypad_bit(ctx, JOYPAD_LEFT, true); break;
|
||||||
|
case LV_KEY_RIGHT: set_joypad_bit(ctx, JOYPAD_RIGHT, true); break;
|
||||||
|
case LV_KEY_ENTER: set_joypad_bit(ctx, JOYPAD_START, true); break;
|
||||||
|
case LV_KEY_ESC: set_joypad_bit(ctx, JOYPAD_SELECT, true); break;
|
||||||
|
default: {
|
||||||
|
if (key== (uint32_t)'z' || key== (uint32_t)'Z') set_joypad_bit(ctx, JOYPAD_A, true);
|
||||||
|
else if (key== (uint32_t)'x' || key== (uint32_t)'X') set_joypad_bit(ctx, JOYPAD_B, true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forward declarations for UI switching */
|
||||||
|
static void build_browser_ui(AppCtx* ctx, lv_obj_t* parent);
|
||||||
|
static void build_emu_ui(AppCtx* ctx, lv_obj_t* parent);
|
||||||
|
static void switch_to_browser(AppCtx* ctx);
|
||||||
|
static void switch_to_emu(AppCtx* ctx);
|
||||||
|
|
||||||
|
/* ROM selection events */
|
||||||
|
static void rom_button_event(lv_event_t* e){
|
||||||
|
AppCtx* ctx=(AppCtx*)lv_event_get_user_data(e);
|
||||||
|
lv_obj_t* btn=lv_event_get_target_obj(e);
|
||||||
|
int* pIdx=(int*)lv_obj_get_user_data(btn);
|
||||||
|
if (!pIdx) return;
|
||||||
|
ctx->selected_rom_idx=*pIdx;
|
||||||
|
if (ctx->selected_rom_idx<0 || ctx->selected_rom_idx>=ctx->rom_count) return;
|
||||||
|
const char* path=ctx->roms[ctx->selected_rom_idx].fullpath;
|
||||||
|
if (load_rom_file(ctx, path)){
|
||||||
|
switch_to_emu(ctx);
|
||||||
|
} else {
|
||||||
|
if (ctx->status_label) lv_label_set_text_fmt(ctx->status_label, "Failed: %s", ctx->roms[ctx->selected_rom_idx].filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void default_rom_event(lv_event_t* e){
|
||||||
|
AppCtx* ctx=(AppCtx*)lv_event_get_user_data(e);
|
||||||
|
if (load_rom_file(ctx, DEFAULT_ROM_PATH)){
|
||||||
|
switch_to_emu(ctx);
|
||||||
|
} else {
|
||||||
|
if (ctx->status_label) lv_label_set_text(ctx->status_label, "default.gb not found in /data/roms/gb/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void back_to_menu_event(lv_event_t* e){
|
||||||
|
AppCtx* ctx=(AppCtx*)lv_event_get_user_data(e);
|
||||||
|
switch_to_browser(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* UI builders */
|
||||||
|
static void build_browser_ui(AppCtx* ctx, lv_obj_t* parent){
|
||||||
|
lv_obj_clean(parent);
|
||||||
|
ctx->rom_list=NULL;
|
||||||
|
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_style_pad_all(parent, 4, 0);
|
||||||
|
lv_obj_set_style_bg_color(parent, lv_color_black(), 0);
|
||||||
|
|
||||||
|
lv_obj_t* info=lv_label_create(parent);
|
||||||
|
lv_label_set_text_fmt(info, "GameBoy (no audio) - Peanut-GB\nPlace ROMs in %s\nDefault: %s\nFound %d ROMs", ROMS_DIR, DEFAULT_ROM_PATH, ctx->rom_count);
|
||||||
|
lv_label_set_long_mode(info, LV_LABEL_LONG_WRAP);
|
||||||
|
lv_obj_set_width(info, LV_PCT(100));
|
||||||
|
lv_obj_set_style_text_color(info, lv_color_white(), 0);
|
||||||
|
|
||||||
|
ctx->status_label=lv_label_create(parent);
|
||||||
|
lv_label_set_text(ctx->status_label, "Select a ROM to start");
|
||||||
|
lv_obj_set_style_text_color(ctx->status_label, lv_palette_main(LV_PALETTE_ORANGE), 0);
|
||||||
|
|
||||||
|
lv_obj_t* list=lv_list_create(parent);
|
||||||
|
lv_obj_set_width(list, LV_PCT(100));
|
||||||
|
lv_obj_set_flex_grow(list, 1);
|
||||||
|
ctx->rom_list=list;
|
||||||
|
|
||||||
|
if (ctx->rom_count==0){
|
||||||
|
lv_obj_t* lbl=lv_label_create(parent);
|
||||||
|
lv_label_set_text(lbl, "No ROMs found in /data/roms/gb\nPut .gb files there.");
|
||||||
|
lv_obj_set_style_text_color(lbl, lv_color_white(), 0);
|
||||||
|
} else {
|
||||||
|
for (int i=0;i<ctx->rom_count;i++){
|
||||||
|
lv_obj_t* btn=lv_list_add_btn(list, LV_SYMBOL_FILE, ctx->roms[i].filename);
|
||||||
|
int* idx=(int*)malloc(sizeof(int)); *idx=i;
|
||||||
|
lv_obj_set_user_data(btn, idx);
|
||||||
|
lv_obj_add_event_cb(btn, rom_button_event, LV_EVENT_CLICKED, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_obj_t* default_btn=lv_btn_create(parent);
|
||||||
|
lv_obj_set_width(default_btn, LV_PCT(100));
|
||||||
|
lv_obj_t* dlbl=lv_label_create(default_btn);
|
||||||
|
lv_label_set_text(dlbl, "Try default.gb");
|
||||||
|
lv_obj_center(dlbl);
|
||||||
|
lv_obj_add_event_cb(default_btn, default_rom_event, LV_EVENT_CLICKED, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void build_emu_ui(AppCtx* ctx, lv_obj_t* parent){
|
||||||
|
lv_obj_clean(parent);
|
||||||
|
lv_obj_set_style_bg_color(parent, lv_color_black(), 0);
|
||||||
|
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_style_pad_all(parent, 0, 0);
|
||||||
|
lv_obj_set_style_pad_row(parent, 0, 0);
|
||||||
|
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
|
||||||
|
lv_obj_t* info_bar=lv_obj_create(parent);
|
||||||
|
lv_obj_set_width(info_bar, LV_PCT(100));
|
||||||
|
lv_obj_set_height(info_bar, LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_style_pad_all(info_bar, 2, 0);
|
||||||
|
lv_obj_set_style_border_width(info_bar, 0, 0);
|
||||||
|
lv_obj_set_flex_flow(info_bar, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_style_bg_color(info_bar, lv_color_hex(0x222222), 0);
|
||||||
|
|
||||||
|
lv_obj_t* title_lbl=lv_label_create(info_bar);
|
||||||
|
ctx->status_label = title_lbl;
|
||||||
|
lv_label_set_text_fmt(title_lbl, "GB: %s FPS:--", ctx->rom_title[0]?ctx->rom_title:"GameBoy");
|
||||||
|
lv_obj_set_style_text_color(title_lbl, lv_color_white(), 0);
|
||||||
|
|
||||||
|
lv_obj_t* spacer=lv_obj_create(info_bar);
|
||||||
|
lv_obj_set_style_bg_opa(spacer, LV_OPA_TRANSP,0);
|
||||||
|
lv_obj_set_style_border_width(spacer,0,0);
|
||||||
|
lv_obj_set_flex_grow(spacer,1);
|
||||||
|
|
||||||
|
lv_obj_t* back_btn=lv_btn_create(info_bar);
|
||||||
|
lv_obj_set_size(back_btn, 60, 28);
|
||||||
|
lv_obj_t* bl=lv_label_create(back_btn); lv_label_set_text(bl,"Menu"); lv_obj_center(bl);
|
||||||
|
lv_obj_add_event_cb(back_btn, back_to_menu_event, LV_EVENT_CLICKED, ctx);
|
||||||
|
|
||||||
|
lv_obj_t* canvas_cont=lv_obj_create(parent);
|
||||||
|
lv_obj_set_width(canvas_cont, LV_PCT(100));
|
||||||
|
lv_obj_set_flex_grow(canvas_cont,1);
|
||||||
|
lv_obj_set_style_bg_color(canvas_cont, lv_color_black(),0);
|
||||||
|
lv_obj_set_style_border_width(canvas_cont,0,0);
|
||||||
|
lv_obj_set_style_pad_all(canvas_cont,2,0);
|
||||||
|
lv_obj_set_flex_flow(canvas_cont, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_flex_align(canvas_cont, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
lv_obj_remove_flag(canvas_cont, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
|
||||||
|
if (!ctx->fb_native){
|
||||||
|
size_t fb_bytes=FRAME_W*FRAME_H*sizeof(uint16_t);
|
||||||
|
ctx->fb_native=(uint16_t*)alloc_psram(fb_bytes);
|
||||||
|
if (ctx->fb_native){
|
||||||
|
ctx->framebuffer_allocated=true;
|
||||||
|
memset(ctx->fb_native,0,fb_bytes);
|
||||||
|
/* Initial grey fill so we can visually confirm buffer ownership even before first gb_run_frame */
|
||||||
|
for(int i=0;i<FRAME_W*FRAME_H;i++) ctx->fb_native[i]=0x4208;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx->fb_native){
|
||||||
|
lv_coord_t disp_w = lv_display_get_horizontal_resolution(NULL);
|
||||||
|
lv_coord_t disp_h = lv_display_get_vertical_resolution(NULL);
|
||||||
|
int avail_h = disp_h - 160;
|
||||||
|
int avail_w = disp_w - 8;
|
||||||
|
int scale = 1;
|
||||||
|
if (avail_w >= FRAME_W * 3 && avail_h >= FRAME_H * 3) scale = 3;
|
||||||
|
else if (avail_w >= FRAME_W * 2 && avail_h >= FRAME_H * 2) scale = 2;
|
||||||
|
|
||||||
|
ctx->canvas = lv_canvas_create(canvas_cont);
|
||||||
|
/* lv_canvas_set_buffer creates internal static_buf header from our raw ptr */
|
||||||
|
lv_canvas_set_buffer(ctx->canvas, ctx->fb_native, FRAME_W, FRAME_H, LV_COLOR_FORMAT_RGB565);
|
||||||
|
ctx->fb_draw_buf = lv_canvas_get_draw_buf(ctx->canvas);
|
||||||
|
if (ctx->fb_draw_buf && ctx->fb_draw_buf->data) {
|
||||||
|
/* Force fresh cache state */
|
||||||
|
lv_draw_buf_invalidate_cache(ctx->fb_draw_buf, NULL);
|
||||||
|
lv_image_cache_drop(ctx->fb_draw_buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scale > 1) {
|
||||||
|
lv_image_set_scale(ctx->canvas, (uint32_t)(256 * scale));
|
||||||
|
lv_image_set_pivot(ctx->canvas, FRAME_W / 2, FRAME_H / 2);
|
||||||
|
}
|
||||||
|
lv_obj_set_style_border_width(ctx->canvas, 1, 0);
|
||||||
|
lv_obj_set_style_border_color(ctx->canvas, lv_color_hex(0x444444), 0);
|
||||||
|
lv_obj_center(ctx->canvas);
|
||||||
|
} else {
|
||||||
|
lv_obj_t* err=lv_label_create(canvas_cont); lv_label_set_text(err,"FB alloc failed"); lv_obj_set_style_text_color(err, lv_color_white(),0);
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_obj_t* ctrl=lv_obj_create(parent);
|
||||||
|
ctx->controls_cont=ctrl;
|
||||||
|
lv_obj_set_width(ctrl, LV_PCT(100));
|
||||||
|
lv_obj_set_height(ctrl, 110);
|
||||||
|
lv_obj_set_style_pad_all(ctrl,2,0);
|
||||||
|
lv_obj_set_style_bg_color(ctrl, lv_color_hex(0x111111),0);
|
||||||
|
lv_obj_set_style_border_width(ctrl,0,0);
|
||||||
|
lv_obj_set_flex_flow(ctrl, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_flex_align(ctrl, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
|
||||||
|
static BtnUserData btn_ud[8];
|
||||||
|
static bool ud_init=false;
|
||||||
|
if (!ud_init){ memset(btn_ud,0,sizeof(btn_ud)); ud_init=true; }
|
||||||
|
|
||||||
|
lv_obj_t* dpad=lv_obj_create(ctrl);
|
||||||
|
lv_obj_set_size(dpad,96,96);
|
||||||
|
lv_obj_set_style_bg_opa(dpad,LV_OPA_TRANSP,0);
|
||||||
|
lv_obj_set_style_border_width(dpad,0,0);
|
||||||
|
lv_obj_set_style_pad_all(dpad,0,0);
|
||||||
|
|
||||||
|
lv_obj_t* up=lv_btn_create(dpad); lv_obj_set_size(up,32,32); lv_obj_set_pos(up,32,0);
|
||||||
|
lv_obj_t* left=lv_btn_create(dpad); lv_obj_set_size(left,32,32); lv_obj_set_pos(left,0,32);
|
||||||
|
lv_obj_t* right=lv_btn_create(dpad); lv_obj_set_size(right,32,32); lv_obj_set_pos(right,64,32);
|
||||||
|
lv_obj_t* down=lv_btn_create(dpad); lv_obj_set_size(down,32,32); lv_obj_set_pos(down,32,64);
|
||||||
|
lv_obj_t* lbl; lbl=lv_label_create(up); lv_label_set_text(lbl, LV_SYMBOL_UP); lv_obj_center(lbl);
|
||||||
|
lbl=lv_label_create(down); lv_label_set_text(lbl, LV_SYMBOL_DOWN); lv_obj_center(lbl);
|
||||||
|
lbl=lv_label_create(left); lv_label_set_text(lbl, LV_SYMBOL_LEFT); lv_obj_center(lbl);
|
||||||
|
lbl=lv_label_create(right); lv_label_set_text(lbl, LV_SYMBOL_RIGHT); lv_obj_center(lbl);
|
||||||
|
|
||||||
|
ctx->btn_up=up; ctx->btn_down=down; ctx->btn_left=left; ctx->btn_right=right;
|
||||||
|
btn_ud[0].ctx=ctx; btn_ud[0].joypad_bit=JOYPAD_UP; lv_obj_add_event_cb(up, input_down_cb, LV_EVENT_PRESSED, &btn_ud[0]); lv_obj_add_event_cb(up, input_up_cb, LV_EVENT_RELEASED, &btn_ud[0]); lv_obj_add_event_cb(up, input_up_cb, LV_EVENT_PRESS_LOST, &btn_ud[0]);
|
||||||
|
btn_ud[1].ctx=ctx; btn_ud[1].joypad_bit=JOYPAD_DOWN; lv_obj_add_event_cb(down, input_down_cb, LV_EVENT_PRESSED, &btn_ud[1]); lv_obj_add_event_cb(down, input_up_cb, LV_EVENT_RELEASED, &btn_ud[1]); lv_obj_add_event_cb(down, input_up_cb, LV_EVENT_PRESS_LOST, &btn_ud[1]);
|
||||||
|
btn_ud[2].ctx=ctx; btn_ud[2].joypad_bit=JOYPAD_LEFT; lv_obj_add_event_cb(left, input_down_cb, LV_EVENT_PRESSED, &btn_ud[2]); lv_obj_add_event_cb(left, input_up_cb, LV_EVENT_RELEASED, &btn_ud[2]); lv_obj_add_event_cb(left, input_up_cb, LV_EVENT_PRESS_LOST, &btn_ud[2]);
|
||||||
|
btn_ud[3].ctx=ctx; btn_ud[3].joypad_bit=JOYPAD_RIGHT;lv_obj_add_event_cb(right, input_down_cb, LV_EVENT_PRESSED, &btn_ud[3]); lv_obj_add_event_cb(right, input_up_cb, LV_EVENT_RELEASED, &btn_ud[3]); lv_obj_add_event_cb(right, input_up_cb, LV_EVENT_PRESS_LOST, &btn_ud[3]);
|
||||||
|
|
||||||
|
lv_obj_t* center_col=lv_obj_create(ctrl);
|
||||||
|
lv_obj_set_size(center_col,64,96);
|
||||||
|
lv_obj_set_style_bg_opa(center_col,LV_OPA_TRANSP,0);
|
||||||
|
lv_obj_set_style_border_width(center_col,0,0);
|
||||||
|
lv_obj_set_flex_flow(center_col, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_flex_align(center_col, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
lv_obj_set_style_pad_row(center_col,4,0);
|
||||||
|
|
||||||
|
lv_obj_t* sel_btn=lv_btn_create(center_col); lv_obj_set_size(sel_btn,60,28); lbl=lv_label_create(sel_btn); lv_label_set_text(lbl,"Sel"); lv_obj_center(lbl);
|
||||||
|
lv_obj_t* sta_btn=lv_btn_create(center_col); lv_obj_set_size(sta_btn,60,28); lbl=lv_label_create(sta_btn); lv_label_set_text(lbl,"Sta"); lv_obj_center(lbl);
|
||||||
|
ctx->btn_select=sel_btn; ctx->btn_start=sta_btn;
|
||||||
|
btn_ud[4].ctx=ctx; btn_ud[4].joypad_bit=JOYPAD_SELECT; lv_obj_add_event_cb(sel_btn, input_down_cb, LV_EVENT_PRESSED, &btn_ud[4]); lv_obj_add_event_cb(sel_btn, input_up_cb, LV_EVENT_RELEASED, &btn_ud[4]); lv_obj_add_event_cb(sel_btn, input_up_cb, LV_EVENT_PRESS_LOST, &btn_ud[4]);
|
||||||
|
btn_ud[5].ctx=ctx; btn_ud[5].joypad_bit=JOYPAD_START; lv_obj_add_event_cb(sta_btn, input_down_cb, LV_EVENT_PRESSED, &btn_ud[5]); lv_obj_add_event_cb(sta_btn, input_up_cb, LV_EVENT_RELEASED, &btn_ud[5]); lv_obj_add_event_cb(sta_btn, input_up_cb, LV_EVENT_PRESS_LOST, &btn_ud[5]);
|
||||||
|
|
||||||
|
lv_obj_t* ab=lv_obj_create(ctrl);
|
||||||
|
lv_obj_set_size(ab,96,96);
|
||||||
|
lv_obj_set_style_bg_opa(ab,LV_OPA_TRANSP,0);
|
||||||
|
lv_obj_set_style_border_width(ab,0,0);
|
||||||
|
lv_obj_set_style_pad_all(ab,0,0);
|
||||||
|
lv_obj_t* b_btn=lv_btn_create(ab); lv_obj_set_size(b_btn,40,40); lv_obj_set_pos(b_btn,0,24); lv_obj_set_style_radius(b_btn,20,0);
|
||||||
|
lv_obj_t* a_btn=lv_btn_create(ab); lv_obj_set_size(a_btn,40,40); lv_obj_set_pos(a_btn,48,8); lv_obj_set_style_radius(a_btn,20,0);
|
||||||
|
lbl=lv_label_create(b_btn); lv_label_set_text(lbl,"B"); lv_obj_center(lbl);
|
||||||
|
lbl=lv_label_create(a_btn); lv_label_set_text(lbl,"A"); lv_obj_center(lbl);
|
||||||
|
ctx->btn_a=a_btn; ctx->btn_b=b_btn;
|
||||||
|
btn_ud[6].ctx=ctx; btn_ud[6].joypad_bit=JOYPAD_B; lv_obj_add_event_cb(b_btn, input_down_cb, LV_EVENT_PRESSED, &btn_ud[6]); lv_obj_add_event_cb(b_btn, input_up_cb, LV_EVENT_RELEASED, &btn_ud[6]); lv_obj_add_event_cb(b_btn, input_up_cb, LV_EVENT_PRESS_LOST, &btn_ud[6]);
|
||||||
|
btn_ud[7].ctx=ctx; btn_ud[7].joypad_bit=JOYPAD_A; lv_obj_add_event_cb(a_btn, input_down_cb, LV_EVENT_PRESSED, &btn_ud[7]); lv_obj_add_event_cb(a_btn, input_up_cb, LV_EVENT_RELEASED, &btn_ud[7]); lv_obj_add_event_cb(a_btn, input_up_cb, LV_EVENT_PRESS_LOST, &btn_ud[7]);
|
||||||
|
|
||||||
|
lv_obj_add_event_cb(parent, key_press_cb, LV_EVENT_KEY, ctx);
|
||||||
|
if (tt_lvgl_hardware_keyboard_is_available()){
|
||||||
|
lv_group_t* g=lv_group_get_default();
|
||||||
|
if (g){ lv_group_add_obj(g, parent); lv_group_focus_obj(parent); lv_group_set_editing(g, true); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx->emu_timer){ lv_timer_delete(ctx->emu_timer); ctx->emu_timer=NULL; }
|
||||||
|
ctx->fps_frames = 0;
|
||||||
|
ctx->fps_last_us = esp_timer_get_time();
|
||||||
|
ctx->lines_drawn = 0;
|
||||||
|
ctx->emu_timer=lv_timer_create(emu_timer_cb, TICK_MS, ctx);
|
||||||
|
ctx->emu_running=true;
|
||||||
|
ctx->mode=APP_MODE_EMU;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void switch_to_browser(AppCtx* ctx){
|
||||||
|
if (ctx->emu_timer){ lv_timer_delete(ctx->emu_timer); ctx->emu_timer=NULL; }
|
||||||
|
ctx->emu_running=false;
|
||||||
|
save_cart_ram(ctx);
|
||||||
|
ctx->mode=APP_MODE_BROWSER;
|
||||||
|
if (!ctx->browser_wrapper || !lv_obj_is_valid(ctx->browser_wrapper)) return;
|
||||||
|
if (ctx->emu_wrapper) lv_obj_add_flag(ctx->emu_wrapper, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
lv_obj_remove_flag(ctx->browser_wrapper, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
scan_rom_dir(ctx);
|
||||||
|
build_browser_ui(ctx, ctx->browser_wrapper);
|
||||||
|
}
|
||||||
|
static void switch_to_emu(AppCtx* ctx){
|
||||||
|
if (!ctx->rom_loaded) return;
|
||||||
|
if (ctx->browser_wrapper) lv_obj_add_flag(ctx->browser_wrapper, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
if (!ctx->emu_wrapper) return;
|
||||||
|
lv_obj_remove_flag(ctx->emu_wrapper, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
build_emu_ui(ctx, ctx->emu_wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lifecycle */
|
||||||
|
static void* create_data(void){
|
||||||
|
AppCtx* ctx=(AppCtx*)calloc(1,sizeof(AppCtx));
|
||||||
|
if (ctx){ ctx->joypad_state=0xFF; ctx->mode=APP_MODE_BROWSER; ctx->selected_rom_idx=-1; }
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
static void destroy_data(void* data){
|
||||||
|
AppCtx* ctx=(AppCtx*)data;
|
||||||
|
if (!ctx) return;
|
||||||
|
if (ctx->emu_timer) lv_timer_delete(ctx->emu_timer);
|
||||||
|
if (ctx->fb_native) heap_caps_free(ctx->fb_native);
|
||||||
|
if (ctx->rom_data) heap_caps_free(ctx->rom_data);
|
||||||
|
if (ctx->cart_ram) heap_caps_free(ctx->cart_ram);
|
||||||
|
free(ctx);
|
||||||
|
}
|
||||||
|
static void on_create(AppHandle app, void* data){
|
||||||
|
AppCtx* ctx=(AppCtx*)data; if (ctx) ctx->app_handle=app;
|
||||||
|
}
|
||||||
|
static void on_destroy(AppHandle app, void* data){ (void)app; (void)data; }
|
||||||
|
static void on_show(AppHandle app, void* data, lv_obj_t* parent){
|
||||||
|
AppCtx* ctx=(AppCtx*)data; if (!ctx) return;
|
||||||
|
ctx->app_handle=app;
|
||||||
|
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_style_pad_all(parent,0,0);
|
||||||
|
lv_obj_set_style_pad_row(parent,0,0);
|
||||||
|
lv_obj_set_style_bg_color(parent, lv_color_black(),0);
|
||||||
|
ctx->toolbar=tt_lvgl_toolbar_create_for_app(parent, app);
|
||||||
|
lv_obj_set_style_bg_color(ctx->toolbar, lv_color_hex(0x111111),0);
|
||||||
|
|
||||||
|
ctx->root_wrapper=lv_obj_create(parent);
|
||||||
|
lv_obj_set_width(ctx->root_wrapper, LV_PCT(100));
|
||||||
|
lv_obj_set_flex_grow(ctx->root_wrapper,1);
|
||||||
|
lv_obj_set_style_pad_all(ctx->root_wrapper,0,0);
|
||||||
|
lv_obj_set_style_border_width(ctx->root_wrapper,0,0);
|
||||||
|
lv_obj_set_style_bg_color(ctx->root_wrapper, lv_color_black(),0);
|
||||||
|
lv_obj_set_flex_flow(ctx->root_wrapper, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_remove_flag(ctx->root_wrapper, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
|
||||||
|
ctx->browser_wrapper=lv_obj_create(ctx->root_wrapper);
|
||||||
|
lv_obj_set_width(ctx->browser_wrapper, LV_PCT(100));
|
||||||
|
lv_obj_set_flex_grow(ctx->browser_wrapper,1);
|
||||||
|
lv_obj_set_style_pad_all(ctx->browser_wrapper,0,0);
|
||||||
|
lv_obj_set_style_border_width(ctx->browser_wrapper,0,0);
|
||||||
|
|
||||||
|
ctx->emu_wrapper=lv_obj_create(ctx->root_wrapper);
|
||||||
|
lv_obj_set_width(ctx->emu_wrapper, LV_PCT(100));
|
||||||
|
lv_obj_set_flex_grow(ctx->emu_wrapper,1);
|
||||||
|
lv_obj_set_style_pad_all(ctx->emu_wrapper,0,0);
|
||||||
|
lv_obj_set_style_border_width(ctx->emu_wrapper,0,0);
|
||||||
|
lv_obj_add_flag(ctx->emu_wrapper, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
|
||||||
|
scan_rom_dir(ctx);
|
||||||
|
struct stat st;
|
||||||
|
bool default_exists=(stat(DEFAULT_ROM_PATH,&st)==0);
|
||||||
|
if (default_exists){
|
||||||
|
if (load_rom_file(ctx, DEFAULT_ROM_PATH)){
|
||||||
|
build_browser_ui(ctx, ctx->browser_wrapper);
|
||||||
|
switch_to_emu(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
build_browser_ui(ctx, ctx->browser_wrapper);
|
||||||
|
lv_obj_remove_flag(ctx->browser_wrapper, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
ctx->mode=APP_MODE_BROWSER;
|
||||||
|
}
|
||||||
|
static void on_hide(AppHandle app, void* data){
|
||||||
|
(void)app;
|
||||||
|
AppCtx* ctx=(AppCtx*)data; if (!ctx) return;
|
||||||
|
if (ctx->emu_timer){ lv_timer_delete(ctx->emu_timer); ctx->emu_timer=NULL; }
|
||||||
|
ctx->emu_running=false;
|
||||||
|
if (ctx->rom_loaded) save_cart_ram(ctx);
|
||||||
|
ctx->canvas=NULL; ctx->fb_draw_buf=NULL; ctx->toolbar=NULL; ctx->root_wrapper=NULL; ctx->browser_wrapper=NULL; ctx->emu_wrapper=NULL;
|
||||||
|
ctx->status_label=NULL; ctx->rom_list=NULL; ctx->controls_cont=NULL;
|
||||||
|
ctx->btn_up=ctx->btn_down=ctx->btn_left=ctx->btn_right=NULL;
|
||||||
|
ctx->btn_a=ctx->btn_b=ctx->btn_start=ctx->btn_select=NULL;
|
||||||
|
ctx->app_handle=NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]){
|
||||||
|
(void)argc; (void)argv;
|
||||||
|
tt_app_register((AppRegistration){
|
||||||
|
.createData=create_data,
|
||||||
|
.destroyData=destroy_data,
|
||||||
|
.onCreate=on_create,
|
||||||
|
.onDestroy=on_destroy,
|
||||||
|
.onShow=on_show,
|
||||||
|
.onHide=on_hide
|
||||||
|
});
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[manifest]
|
||||||
|
version=0.1
|
||||||
|
[target]
|
||||||
|
sdk=0.8.0-dev
|
||||||
|
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
|
[app]
|
||||||
|
id=one.tactility.gameboy
|
||||||
|
versionName=0.1.0-dev
|
||||||
|
versionCode=1
|
||||||
|
name=GameBoy
|
||||||
|
description=DMG Game Boy emulator (no audio prototype, Peanut-GB)
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
[manifest]
|
manifest.version=0.2
|
||||||
version=0.1
|
target.sdk=0.8.0-dev
|
||||||
[target]
|
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
sdk=0.7.0-dev
|
app.id=one.tactility.graphicsdemo
|
||||||
platforms=esp32,esp32s3,esp32c6,esp32p4
|
app.version.name=0.6.0
|
||||||
[app]
|
app.version.code=6
|
||||||
id=one.tactility.graphicsdemo
|
app.name=Graphics Demo
|
||||||
versionName=0.3.0
|
|
||||||
versionCode=3
|
|
||||||
name=Graphics Demo
|
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
[manifest]
|
manifest.version=0.2
|
||||||
version=0.1
|
target.sdk=0.8.0-dev
|
||||||
[target]
|
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
sdk=0.7.0-dev
|
app.id=one.tactility.helloworld
|
||||||
platforms=esp32s3
|
app.version.name=0.6.0
|
||||||
[app]
|
app.version.code=6
|
||||||
id=one.tactility.helloworld
|
app.name=Hello World
|
||||||
versionName=0.3.0
|
|
||||||
versionCode=3
|
|
||||||
name=Hello World
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
|
|
||||||
|
if (DEFINED ENV{TACTILITY_SDK_PATH})
|
||||||
|
set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH})
|
||||||
|
else()
|
||||||
|
set(TACTILITY_SDK_PATH "../../release/TactilitySDK")
|
||||||
|
message(WARNING "⚠️ TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake")
|
||||||
|
set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH})
|
||||||
|
|
||||||
|
project(ImmichElias)
|
||||||
|
tactility_project(ImmichElias)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
file(GLOB_RECURSE SOURCE_FILES Source/*.c)
|
||||||
|
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
|
||||||
|
)
|
||||||
|
target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-format-truncation -Wno-unused-but-set-variable -Wno-unused-function -Wno-unused-variable)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
|||||||
|
manifest.version=0.2
|
||||||
|
target.sdk=0.8.0-dev
|
||||||
|
target.platforms=esp32s3,esp32p4
|
||||||
|
app.id=one.tactility.imichelias
|
||||||
|
app.version.name=0.1.0
|
||||||
|
app.version.code=1
|
||||||
|
app.name=Immich Elias
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
|
|
||||||
|
if (DEFINED ENV{TACTILITY_SDK_PATH})
|
||||||
|
set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH})
|
||||||
|
else()
|
||||||
|
set(TACTILITY_SDK_PATH "../../release/TactilitySDK")
|
||||||
|
message(WARNING "⚠️ TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake")
|
||||||
|
set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH})
|
||||||
|
|
||||||
|
project(LuaGame)
|
||||||
|
tactility_project(LuaGame)
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# LuaGame - Lua Interpreter Game Engine
|
||||||
|
|
||||||
|
External ELF app that runs Lua games on Tactility OS (ES3C35P 320x480 tested on 192.168.68.107).
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
- Embeds Lua 5.4.7 source (no external dependency, avoids missing symbol issues)
|
||||||
|
- Lua init + game loop run in dedicated 16K stack FreeRTOS task to avoid GUI task (4096 bytes) stack overflow
|
||||||
|
- Game framebuffer: 240x240 RGB565 in PSRAM, rendered via LVGL canvas (`lv_canvas_set_buffer` + `lv_obj_invalidate` with `tt_lvgl_lock`)
|
||||||
|
- Direct framebuffer text using 8x8 bitmap font (`font8x8.h`) – 17+ FPS vs 15 FPS with LVGL labels
|
||||||
|
- **Sys API** (original Tactility breakout.lua):
|
||||||
|
- `sys.log(msg)`
|
||||||
|
- `sys.clear(hexColor)`
|
||||||
|
- `sys.print(x,y,text,hexColor)` – direct FB via `draw_text8`
|
||||||
|
- `sys.rect(x,y,w,h,hexColor,filled)` – border or filled
|
||||||
|
- `sys.circle(x,y,r,hexColor,filled)` – midpoint + filled scan
|
||||||
|
- `sys.line(x0,y0,x1,y1,hexColor)`
|
||||||
|
- `sys.touch() -> pressed, tx, ty` (touch mapped to 240 game space)
|
||||||
|
- `sys.tone(note, duration, waveform, volume)` → SfxEngine `playNote`
|
||||||
|
- `sys.sfx(name)` → SfxEngine `play(SfxId)` mapping: confirm, coin, hurt, gameover, levelup, brickhit, etc.
|
||||||
|
- **Pico API** compatibility (from `/Users/adolforeyna/Projects/pico-bare-metal/Adolfo/basic1/games/lua_examples`):
|
||||||
|
- `game.width()`, `height()`, `set_frame_updates(bool)`, `exit()`, `game.vars` persistent table
|
||||||
|
- `renderer.clear(white)`, `pixel(x,y,on)`, `line(x0,y0,x1,y1,on,width)`, `rect(x,y,w,h,on,filled)`, `circle(x,y,r,on,filled)`, `triangle(x0,y0,x1,y1,x2,y2,on,filled)`, `text(x,y,txt,on)`, `text_scaled(x,y,txt,on,scale)` (scale 1-4)
|
||||||
|
- `INPUT` constants: `NONE=0, TOUCH_DOWN=1, TOUCH_MOVE=2, TOUCH_UP=3, BUTTON_0=4, BUTTON_1=5, FRAME_TICK=6, GESTURE=7`
|
||||||
|
- Event system: generates `TOUCH_DOWN/MOVE/UP` from touch state + `FRAME_TICK` every frame, calls `update(event)` returning bool redraw
|
||||||
|
- Detection: if source contains `renderer.` → Pico mode, else sys mode
|
||||||
|
- Embedded games:
|
||||||
|
- `embedded_breakout_lua_src` – fast breakout (ball 4.5 vs 2, paddle 0.25 spin) – default
|
||||||
|
- `pico_games.h` auto-generated with 17 games: 2048, air_hockey, asteroids, ball, breakout, counter, flappy_bird, lunar_lander, memory_match, pacman, pong, simon_says, snake, solitaire, tetris, tic_tac_toe – tested Pong runs at 17.5 FPS
|
||||||
|
- File loading: PSRAM buffer + `luaL_loadbuffer` to avoid `luaL_loadfile` stack issues, supports SD paths `/sdcard/lua/*.lua`
|
||||||
|
|
||||||
|
## Symbol fixes
|
||||||
|
Previous missing: `clearerr`, `clock`, `getenv`, `gmtime`, `setlocale`, `setvbuf`, `tmpfile`, `tmpnam`, `ungetc`, `lv_canvas_get_draw_buf`, `lv_draw_buf_invalidate_cache`, `lv_image_cache_drop`.
|
||||||
|
Fixed by local stubs + avoiding unexported APIs, only `lv_canvas_create`, `lv_canvas_set_buffer`, `lv_obj_invalidate`, `tt_lvgl_lock/unlock`.
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
- Ball speed: 2 → 4.5 (2.25x faster), configurable in embedded string
|
||||||
|
- FPS: 15 → 17.5 FPS after direct FB text + dedicated game loop task
|
||||||
|
- Memory: internal low ~2-3KB (PSRAM used for FB and Lua heap), stable, no crash 20s+
|
||||||
|
|
||||||
|
## Build / Install
|
||||||
|
```bash
|
||||||
|
cd /Users/adolforeyna/Projects/Tactility/apps
|
||||||
|
unset PYTHONPATH; unset PYTHONHOME
|
||||||
|
export IDF_PYTHON_ENV_PATH=/Users/adolforeyna/.espressif/python_env/idf5.3_py3.9_env
|
||||||
|
source /Users/adolforeyna/esp/esp-idf/export.sh
|
||||||
|
export TACTILITY_SDK_PATH=/Users/adolforeyna/Projects/Tactility/firmware/release/TactilitySDK
|
||||||
|
export ESP_IDF_VERSION=5.5
|
||||||
|
python tactility.py Apps/LuaGame build esp32s3 --local-sdk
|
||||||
|
curl -X PUT http://192.168.68.107/api/apps/install -F "file=@Apps/LuaGame/build/LuaGame.app"
|
||||||
|
curl -X POST "http://192.168.68.107/api/apps/run?id=one.tactility.luagame"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Porting Pico Games
|
||||||
|
Yes – all 17 examples from `pico-bare-metal/Adolfo/basic1/games/lua_examples` use:
|
||||||
|
- `game.vars` state machine
|
||||||
|
- `renderer.*` monochrome API (boolean on/off → mapped to white/black RGB565)
|
||||||
|
- `INPUT.TOUCH_*` + `FRAME_TICK`
|
||||||
|
|
||||||
|
Our engine now implements compatibility shim in C (see `l_renderer_*`, `l_game_*`, INPUT table). Pong tested:
|
||||||
|
```
|
||||||
|
[LuaGame] using Pico test game: Pong (7538 bytes)
|
||||||
|
Pong initialized
|
||||||
|
FPS 17.5
|
||||||
|
```
|
||||||
|
Other games (snake, tetris, 2048, etc.) should work same – they are ~50-880 lines, no file I/O, pure math/table. To add selector UI, list `pico_games[]` via LVGL list and load selected `src`.
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
- Game selector UI (menu_wrapper + game_wrapper, hide/show)
|
||||||
|
- Copy pico examples to SD via USB MSC or bundle selector
|
||||||
|
- Audio mapping for Pico games (currently sys.sfx only, could map to renderer events)
|
||||||
|
- Scale canvas to full 320x480 or dynamic `game.width/height`
|
||||||
|
- Fix internal memory low warnings (reduce SfxEngine voices or Lua heap)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
|
||||||
|
file(GLOB_RECURSE SFX_ENGINE_FILES ../../../Libraries/SfxEngine/Source/*.c*)
|
||||||
|
|
||||||
|
idf_component_register(
|
||||||
|
SRCS ${SOURCE_FILES} ${SFX_ENGINE_FILES}
|
||||||
|
INCLUDE_DIRS Source Source/lua ../../../Libraries/SfxEngine/Include ../../../Libraries/TactilityCpp/Include
|
||||||
|
REQUIRES TactilitySDK
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-unused-variable -Wno-unused-function -Wno-implicit-fallthrough -Wno-sign-compare -Wno-unused-but-set-variable -Wno-missing-field-initializers -Wno-misleading-indentation)
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#include "audio.h"
|
||||||
|
#include "SfxEngine.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <strings.h>
|
||||||
|
|
||||||
|
static SfxEngine* g_engine = nullptr;
|
||||||
|
|
||||||
|
void audio_init(void) {
|
||||||
|
if (g_engine) return;
|
||||||
|
g_engine = new SfxEngine();
|
||||||
|
if (!g_engine->start()) {
|
||||||
|
printf("[Audio] SfxEngine start failed\n");
|
||||||
|
delete g_engine;
|
||||||
|
g_engine = nullptr;
|
||||||
|
} else {
|
||||||
|
printf("[Audio] SfxEngine started\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void audio_deinit(void) {
|
||||||
|
if (g_engine) {
|
||||||
|
g_engine->stop();
|
||||||
|
delete g_engine;
|
||||||
|
g_engine = nullptr;
|
||||||
|
printf("[Audio] SfxEngine stopped\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static SfxId sfx_name_to_id(const char* name) {
|
||||||
|
if (!name) return SfxId::Click;
|
||||||
|
if (strcasecmp(name, "confirm")==0) return SfxId::Confirm;
|
||||||
|
if (strcasecmp(name, "coin")==0) return SfxId::Coin;
|
||||||
|
if (strcasecmp(name, "hurt")==0) return SfxId::Hurt;
|
||||||
|
if (strcasecmp(name, "gameover")==0) return SfxId::GameOver;
|
||||||
|
if (strcasecmp(name, "levelup")==0) return SfxId::LevelUp;
|
||||||
|
if (strcasecmp(name, "powerup")==0) return SfxId::Powerup;
|
||||||
|
if (strcasecmp(name, "brickhit")==0) return SfxId::BrickHit;
|
||||||
|
if (strcasecmp(name, "click")==0) return SfxId::Click;
|
||||||
|
if (strcasecmp(name, "blip")==0) return SfxId::Blip;
|
||||||
|
if (strcasecmp(name, "jump")==0) return SfxId::Jump;
|
||||||
|
if (strcasecmp(name, "laser")==0) return SfxId::Laser;
|
||||||
|
if (strcasecmp(name, "explosion")==0) return SfxId::Explosion;
|
||||||
|
if (strcasecmp(name, "pickup")==0) return SfxId::Pickup;
|
||||||
|
if (strcasecmp(name, "alert")==0) return SfxId::Alert;
|
||||||
|
if (strcasecmp(name, "success")==0) return SfxId::Success;
|
||||||
|
// default mapping
|
||||||
|
return SfxId::Click;
|
||||||
|
}
|
||||||
|
|
||||||
|
void audio_play_sfx(const char* name) {
|
||||||
|
if (!g_engine) return;
|
||||||
|
SfxId id = sfx_name_to_id(name);
|
||||||
|
g_engine->play(id);
|
||||||
|
printf("[Audio] sfx %s -> %d\n", name, (int)id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void audio_play_tone(int note, int duration_ms, const char* waveform, float volume) {
|
||||||
|
if (!g_engine) return;
|
||||||
|
if (volume <=0) volume = 0.5f;
|
||||||
|
if (duration_ms <=0) duration_ms = 80;
|
||||||
|
// waveform string: sine, square, triangle, etc
|
||||||
|
SfxWaveType wave = SfxWaveType::Square;
|
||||||
|
if (waveform) {
|
||||||
|
if (strcasecmp(waveform,"sine")==0) wave = SfxWaveType::Sine;
|
||||||
|
else if (strcasecmp(waveform,"triangle")==0) wave = SfxWaveType::Triangle;
|
||||||
|
else if (strcasecmp(waveform,"square")==0) wave = SfxWaveType::Square;
|
||||||
|
else if (strcasecmp(waveform,"saw")==0 || strcasecmp(waveform,"sawtooth")==0) wave = SfxWaveType::Sawtooth;
|
||||||
|
else if (strcasecmp(waveform,"noise")==0) wave = SfxWaveType::Noise;
|
||||||
|
else if (strcasecmp(waveform,"pulse")==0) wave = SfxWaveType::Pulse25;
|
||||||
|
}
|
||||||
|
// Use voice 0 for tone, playNote API: voice, pitch, durationMs, wave, volume...
|
||||||
|
// SfxEngine has playNote? Check header: playNote exists.
|
||||||
|
// The signature in SfxEngine: bool playNote(uint8_t voice, uint8_t pitch, ...)
|
||||||
|
// We'll use simplified: play note on voice 0
|
||||||
|
g_engine->playNote(0, (uint8_t)note, (uint16_t)duration_ms, wave, volume);
|
||||||
|
printf("[Audio] tone note=%d dur=%d wave=%s vol=%.2f\n", note, duration_ms, waveform?waveform:"-", volume);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void audio_init(void);
|
||||||
|
void audio_deinit(void);
|
||||||
|
void audio_play_sfx(const char* name);
|
||||||
|
void audio_play_tone(int note, int duration_ms, const char* waveform, float volume);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
/**
|
||||||
|
* 8x8 monochrome bitmap fonts for rendering
|
||||||
|
* Author: Daniel Hepper <daniel@hepper.net>
|
||||||
|
*
|
||||||
|
* License: Public Domain
|
||||||
|
*
|
||||||
|
* Based on:
|
||||||
|
* // Summary: font8x8.h
|
||||||
|
* // 8x8 monochrome bitmap fonts for rendering
|
||||||
|
* //
|
||||||
|
* // Author:
|
||||||
|
* // Marcel Sondaar
|
||||||
|
* // International Business Machines (public domain VGA fonts)
|
||||||
|
* //
|
||||||
|
* // License:
|
||||||
|
* // Public Domain
|
||||||
|
*
|
||||||
|
* Fetched from: http://dimensionalrift.homelinux.net/combuster/mos3/?p=viewsource&file=/modules/gfx/font8_8.asm
|
||||||
|
**/
|
||||||
|
|
||||||
|
// Constant: font8x8_basic
|
||||||
|
// Contains an 8x8 font map for unicode points U+0000 - U+007F (basic latin)
|
||||||
|
char font8x8_basic[128][8] = {
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0000 (nul)
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0001
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0002
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0003
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0004
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0005
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0006
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0007
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0008
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0009
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000A
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000B
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000C
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000D
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000E
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000F
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0010
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0011
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0012
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0013
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0014
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0015
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0016
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0017
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0018
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0019
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001A
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001B
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001C
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001D
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001E
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001F
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0020 (space)
|
||||||
|
{ 0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00}, // U+0021 (!)
|
||||||
|
{ 0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0022 (")
|
||||||
|
{ 0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00}, // U+0023 (#)
|
||||||
|
{ 0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00}, // U+0024 ($)
|
||||||
|
{ 0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00}, // U+0025 (%)
|
||||||
|
{ 0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00}, // U+0026 (&)
|
||||||
|
{ 0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0027 (')
|
||||||
|
{ 0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00}, // U+0028 (()
|
||||||
|
{ 0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00}, // U+0029 ())
|
||||||
|
{ 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00}, // U+002A (*)
|
||||||
|
{ 0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00}, // U+002B (+)
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // U+002C (,)
|
||||||
|
{ 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00}, // U+002D (-)
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // U+002E (.)
|
||||||
|
{ 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00}, // U+002F (/)
|
||||||
|
{ 0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00}, // U+0030 (0)
|
||||||
|
{ 0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00}, // U+0031 (1)
|
||||||
|
{ 0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00}, // U+0032 (2)
|
||||||
|
{ 0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00}, // U+0033 (3)
|
||||||
|
{ 0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00}, // U+0034 (4)
|
||||||
|
{ 0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00}, // U+0035 (5)
|
||||||
|
{ 0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00}, // U+0036 (6)
|
||||||
|
{ 0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00}, // U+0037 (7)
|
||||||
|
{ 0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00}, // U+0038 (8)
|
||||||
|
{ 0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00}, // U+0039 (9)
|
||||||
|
{ 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // U+003A (:)
|
||||||
|
{ 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // U+003B (;)
|
||||||
|
{ 0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00}, // U+003C (<)
|
||||||
|
{ 0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00}, // U+003D (=)
|
||||||
|
{ 0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00}, // U+003E (>)
|
||||||
|
{ 0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00}, // U+003F (?)
|
||||||
|
{ 0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00}, // U+0040 (@)
|
||||||
|
{ 0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00}, // U+0041 (A)
|
||||||
|
{ 0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00}, // U+0042 (B)
|
||||||
|
{ 0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00}, // U+0043 (C)
|
||||||
|
{ 0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00}, // U+0044 (D)
|
||||||
|
{ 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00}, // U+0045 (E)
|
||||||
|
{ 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00}, // U+0046 (F)
|
||||||
|
{ 0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00}, // U+0047 (G)
|
||||||
|
{ 0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00}, // U+0048 (H)
|
||||||
|
{ 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0049 (I)
|
||||||
|
{ 0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00}, // U+004A (J)
|
||||||
|
{ 0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00}, // U+004B (K)
|
||||||
|
{ 0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00}, // U+004C (L)
|
||||||
|
{ 0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00}, // U+004D (M)
|
||||||
|
{ 0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00}, // U+004E (N)
|
||||||
|
{ 0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00}, // U+004F (O)
|
||||||
|
{ 0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00}, // U+0050 (P)
|
||||||
|
{ 0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00}, // U+0051 (Q)
|
||||||
|
{ 0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00}, // U+0052 (R)
|
||||||
|
{ 0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00}, // U+0053 (S)
|
||||||
|
{ 0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0054 (T)
|
||||||
|
{ 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00}, // U+0055 (U)
|
||||||
|
{ 0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // U+0056 (V)
|
||||||
|
{ 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00}, // U+0057 (W)
|
||||||
|
{ 0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00}, // U+0058 (X)
|
||||||
|
{ 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00}, // U+0059 (Y)
|
||||||
|
{ 0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00}, // U+005A (Z)
|
||||||
|
{ 0x1E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1E, 0x00}, // U+005B ([)
|
||||||
|
{ 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00}, // U+005C (\)
|
||||||
|
{ 0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x00}, // U+005D (])
|
||||||
|
{ 0x08, 0x1C, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00}, // U+005E (^)
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF}, // U+005F (_)
|
||||||
|
{ 0x0C, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0060 (`)
|
||||||
|
{ 0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00}, // U+0061 (a)
|
||||||
|
{ 0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00}, // U+0062 (b)
|
||||||
|
{ 0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00}, // U+0063 (c)
|
||||||
|
{ 0x38, 0x30, 0x30, 0x3e, 0x33, 0x33, 0x6E, 0x00}, // U+0064 (d)
|
||||||
|
{ 0x00, 0x00, 0x1E, 0x33, 0x3f, 0x03, 0x1E, 0x00}, // U+0065 (e)
|
||||||
|
{ 0x1C, 0x36, 0x06, 0x0f, 0x06, 0x06, 0x0F, 0x00}, // U+0066 (f)
|
||||||
|
{ 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // U+0067 (g)
|
||||||
|
{ 0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00}, // U+0068 (h)
|
||||||
|
{ 0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0069 (i)
|
||||||
|
{ 0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E}, // U+006A (j)
|
||||||
|
{ 0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00}, // U+006B (k)
|
||||||
|
{ 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+006C (l)
|
||||||
|
{ 0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00}, // U+006D (m)
|
||||||
|
{ 0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00}, // U+006E (n)
|
||||||
|
{ 0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00}, // U+006F (o)
|
||||||
|
{ 0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F}, // U+0070 (p)
|
||||||
|
{ 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78}, // U+0071 (q)
|
||||||
|
{ 0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00}, // U+0072 (r)
|
||||||
|
{ 0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00}, // U+0073 (s)
|
||||||
|
{ 0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00}, // U+0074 (t)
|
||||||
|
{ 0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00}, // U+0075 (u)
|
||||||
|
{ 0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // U+0076 (v)
|
||||||
|
{ 0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00}, // U+0077 (w)
|
||||||
|
{ 0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00}, // U+0078 (x)
|
||||||
|
{ 0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // U+0079 (y)
|
||||||
|
{ 0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00}, // U+007A (z)
|
||||||
|
{ 0x38, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x38, 0x00}, // U+007B ({)
|
||||||
|
{ 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00}, // U+007C (|)
|
||||||
|
{ 0x07, 0x0C, 0x0C, 0x38, 0x0C, 0x0C, 0x07, 0x00}, // U+007D (})
|
||||||
|
{ 0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+007E (~)
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // U+007F
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
** $Id: lapi.h $
|
||||||
|
** Auxiliary functions from Lua API
|
||||||
|
** See Copyright Notice in lua.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef lapi_h
|
||||||
|
#define lapi_h
|
||||||
|
|
||||||
|
|
||||||
|
#include "llimits.h"
|
||||||
|
#include "lstate.h"
|
||||||
|
|
||||||
|
|
||||||
|
/* Increments 'L->top.p', checking for stack overflows */
|
||||||
|
#define api_incr_top(L) {L->top.p++; \
|
||||||
|
api_check(L, L->top.p <= L->ci->top.p, \
|
||||||
|
"stack overflow");}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** If a call returns too many multiple returns, the callee may not have
|
||||||
|
** stack space to accommodate all results. In this case, this macro
|
||||||
|
** increases its stack space ('L->ci->top.p').
|
||||||
|
*/
|
||||||
|
#define adjustresults(L,nres) \
|
||||||
|
{ if ((nres) <= LUA_MULTRET && L->ci->top.p < L->top.p) \
|
||||||
|
L->ci->top.p = L->top.p; }
|
||||||
|
|
||||||
|
|
||||||
|
/* Ensure the stack has at least 'n' elements */
|
||||||
|
#define api_checknelems(L,n) \
|
||||||
|
api_check(L, (n) < (L->top.p - L->ci->func.p), \
|
||||||
|
"not enough elements in the stack")
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** To reduce the overhead of returning from C functions, the presence of
|
||||||
|
** to-be-closed variables in these functions is coded in the CallInfo's
|
||||||
|
** field 'nresults', in a way that functions with no to-be-closed variables
|
||||||
|
** with zero, one, or "all" wanted results have no overhead. Functions
|
||||||
|
** with other number of wanted results, as well as functions with
|
||||||
|
** variables to be closed, have an extra check.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define hastocloseCfunc(n) ((n) < LUA_MULTRET)
|
||||||
|
|
||||||
|
/* Map [-1, inf) (range of 'nresults') into (-inf, -2] */
|
||||||
|
#define codeNresults(n) (-(n) - 3)
|
||||||
|
#define decodeNresults(n) (-(n) - 3)
|
||||||
|
|
||||||
|
#endif
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,301 @@
|
|||||||
|
/*
|
||||||
|
** $Id: lauxlib.h $
|
||||||
|
** Auxiliary functions for building Lua libraries
|
||||||
|
** See Copyright Notice in lua.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef lauxlib_h
|
||||||
|
#define lauxlib_h
|
||||||
|
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#include "luaconf.h"
|
||||||
|
#include "lua.h"
|
||||||
|
|
||||||
|
|
||||||
|
/* global table */
|
||||||
|
#define LUA_GNAME "_G"
|
||||||
|
|
||||||
|
|
||||||
|
typedef struct luaL_Buffer luaL_Buffer;
|
||||||
|
|
||||||
|
|
||||||
|
/* extra error code for 'luaL_loadfilex' */
|
||||||
|
#define LUA_ERRFILE (LUA_ERRERR+1)
|
||||||
|
|
||||||
|
|
||||||
|
/* key, in the registry, for table of loaded modules */
|
||||||
|
#define LUA_LOADED_TABLE "_LOADED"
|
||||||
|
|
||||||
|
|
||||||
|
/* key, in the registry, for table of preloaded loaders */
|
||||||
|
#define LUA_PRELOAD_TABLE "_PRELOAD"
|
||||||
|
|
||||||
|
|
||||||
|
typedef struct luaL_Reg {
|
||||||
|
const char *name;
|
||||||
|
lua_CFunction func;
|
||||||
|
} luaL_Reg;
|
||||||
|
|
||||||
|
|
||||||
|
#define LUAL_NUMSIZES (sizeof(lua_Integer)*16 + sizeof(lua_Number))
|
||||||
|
|
||||||
|
LUALIB_API void (luaL_checkversion_) (lua_State *L, lua_Number ver, size_t sz);
|
||||||
|
#define luaL_checkversion(L) \
|
||||||
|
luaL_checkversion_(L, LUA_VERSION_NUM, LUAL_NUMSIZES)
|
||||||
|
|
||||||
|
LUALIB_API int (luaL_getmetafield) (lua_State *L, int obj, const char *e);
|
||||||
|
LUALIB_API int (luaL_callmeta) (lua_State *L, int obj, const char *e);
|
||||||
|
LUALIB_API const char *(luaL_tolstring) (lua_State *L, int idx, size_t *len);
|
||||||
|
LUALIB_API int (luaL_argerror) (lua_State *L, int arg, const char *extramsg);
|
||||||
|
LUALIB_API int (luaL_typeerror) (lua_State *L, int arg, const char *tname);
|
||||||
|
LUALIB_API const char *(luaL_checklstring) (lua_State *L, int arg,
|
||||||
|
size_t *l);
|
||||||
|
LUALIB_API const char *(luaL_optlstring) (lua_State *L, int arg,
|
||||||
|
const char *def, size_t *l);
|
||||||
|
LUALIB_API lua_Number (luaL_checknumber) (lua_State *L, int arg);
|
||||||
|
LUALIB_API lua_Number (luaL_optnumber) (lua_State *L, int arg, lua_Number def);
|
||||||
|
|
||||||
|
LUALIB_API lua_Integer (luaL_checkinteger) (lua_State *L, int arg);
|
||||||
|
LUALIB_API lua_Integer (luaL_optinteger) (lua_State *L, int arg,
|
||||||
|
lua_Integer def);
|
||||||
|
|
||||||
|
LUALIB_API void (luaL_checkstack) (lua_State *L, int sz, const char *msg);
|
||||||
|
LUALIB_API void (luaL_checktype) (lua_State *L, int arg, int t);
|
||||||
|
LUALIB_API void (luaL_checkany) (lua_State *L, int arg);
|
||||||
|
|
||||||
|
LUALIB_API int (luaL_newmetatable) (lua_State *L, const char *tname);
|
||||||
|
LUALIB_API void (luaL_setmetatable) (lua_State *L, const char *tname);
|
||||||
|
LUALIB_API void *(luaL_testudata) (lua_State *L, int ud, const char *tname);
|
||||||
|
LUALIB_API void *(luaL_checkudata) (lua_State *L, int ud, const char *tname);
|
||||||
|
|
||||||
|
LUALIB_API void (luaL_where) (lua_State *L, int lvl);
|
||||||
|
LUALIB_API int (luaL_error) (lua_State *L, const char *fmt, ...);
|
||||||
|
|
||||||
|
LUALIB_API int (luaL_checkoption) (lua_State *L, int arg, const char *def,
|
||||||
|
const char *const lst[]);
|
||||||
|
|
||||||
|
LUALIB_API int (luaL_fileresult) (lua_State *L, int stat, const char *fname);
|
||||||
|
LUALIB_API int (luaL_execresult) (lua_State *L, int stat);
|
||||||
|
|
||||||
|
|
||||||
|
/* predefined references */
|
||||||
|
#define LUA_NOREF (-2)
|
||||||
|
#define LUA_REFNIL (-1)
|
||||||
|
|
||||||
|
LUALIB_API int (luaL_ref) (lua_State *L, int t);
|
||||||
|
LUALIB_API void (luaL_unref) (lua_State *L, int t, int ref);
|
||||||
|
|
||||||
|
LUALIB_API int (luaL_loadfilex) (lua_State *L, const char *filename,
|
||||||
|
const char *mode);
|
||||||
|
|
||||||
|
#define luaL_loadfile(L,f) luaL_loadfilex(L,f,NULL)
|
||||||
|
|
||||||
|
LUALIB_API int (luaL_loadbufferx) (lua_State *L, const char *buff, size_t sz,
|
||||||
|
const char *name, const char *mode);
|
||||||
|
LUALIB_API int (luaL_loadstring) (lua_State *L, const char *s);
|
||||||
|
|
||||||
|
LUALIB_API lua_State *(luaL_newstate) (void);
|
||||||
|
|
||||||
|
LUALIB_API lua_Integer (luaL_len) (lua_State *L, int idx);
|
||||||
|
|
||||||
|
LUALIB_API void (luaL_addgsub) (luaL_Buffer *b, const char *s,
|
||||||
|
const char *p, const char *r);
|
||||||
|
LUALIB_API const char *(luaL_gsub) (lua_State *L, const char *s,
|
||||||
|
const char *p, const char *r);
|
||||||
|
|
||||||
|
LUALIB_API void (luaL_setfuncs) (lua_State *L, const luaL_Reg *l, int nup);
|
||||||
|
|
||||||
|
LUALIB_API int (luaL_getsubtable) (lua_State *L, int idx, const char *fname);
|
||||||
|
|
||||||
|
LUALIB_API void (luaL_traceback) (lua_State *L, lua_State *L1,
|
||||||
|
const char *msg, int level);
|
||||||
|
|
||||||
|
LUALIB_API void (luaL_requiref) (lua_State *L, const char *modname,
|
||||||
|
lua_CFunction openf, int glb);
|
||||||
|
|
||||||
|
/*
|
||||||
|
** ===============================================================
|
||||||
|
** some useful macros
|
||||||
|
** ===============================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
#define luaL_newlibtable(L,l) \
|
||||||
|
lua_createtable(L, 0, sizeof(l)/sizeof((l)[0]) - 1)
|
||||||
|
|
||||||
|
#define luaL_newlib(L,l) \
|
||||||
|
(luaL_checkversion(L), luaL_newlibtable(L,l), luaL_setfuncs(L,l,0))
|
||||||
|
|
||||||
|
#define luaL_argcheck(L, cond,arg,extramsg) \
|
||||||
|
((void)(luai_likely(cond) || luaL_argerror(L, (arg), (extramsg))))
|
||||||
|
|
||||||
|
#define luaL_argexpected(L,cond,arg,tname) \
|
||||||
|
((void)(luai_likely(cond) || luaL_typeerror(L, (arg), (tname))))
|
||||||
|
|
||||||
|
#define luaL_checkstring(L,n) (luaL_checklstring(L, (n), NULL))
|
||||||
|
#define luaL_optstring(L,n,d) (luaL_optlstring(L, (n), (d), NULL))
|
||||||
|
|
||||||
|
#define luaL_typename(L,i) lua_typename(L, lua_type(L,(i)))
|
||||||
|
|
||||||
|
#define luaL_dofile(L, fn) \
|
||||||
|
(luaL_loadfile(L, fn) || lua_pcall(L, 0, LUA_MULTRET, 0))
|
||||||
|
|
||||||
|
#define luaL_dostring(L, s) \
|
||||||
|
(luaL_loadstring(L, s) || lua_pcall(L, 0, LUA_MULTRET, 0))
|
||||||
|
|
||||||
|
#define luaL_getmetatable(L,n) (lua_getfield(L, LUA_REGISTRYINDEX, (n)))
|
||||||
|
|
||||||
|
#define luaL_opt(L,f,n,d) (lua_isnoneornil(L,(n)) ? (d) : f(L,(n)))
|
||||||
|
|
||||||
|
#define luaL_loadbuffer(L,s,sz,n) luaL_loadbufferx(L,s,sz,n,NULL)
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Perform arithmetic operations on lua_Integer values with wrap-around
|
||||||
|
** semantics, as the Lua core does.
|
||||||
|
*/
|
||||||
|
#define luaL_intop(op,v1,v2) \
|
||||||
|
((lua_Integer)((lua_Unsigned)(v1) op (lua_Unsigned)(v2)))
|
||||||
|
|
||||||
|
|
||||||
|
/* push the value used to represent failure/error */
|
||||||
|
#define luaL_pushfail(L) lua_pushnil(L)
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Internal assertions for in-house debugging
|
||||||
|
*/
|
||||||
|
#if !defined(lua_assert)
|
||||||
|
|
||||||
|
#if defined LUAI_ASSERT
|
||||||
|
#include <assert.h>
|
||||||
|
#define lua_assert(c) assert(c)
|
||||||
|
#else
|
||||||
|
#define lua_assert(c) ((void)0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** {======================================================
|
||||||
|
** Generic Buffer manipulation
|
||||||
|
** =======================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
struct luaL_Buffer {
|
||||||
|
char *b; /* buffer address */
|
||||||
|
size_t size; /* buffer size */
|
||||||
|
size_t n; /* number of characters in buffer */
|
||||||
|
lua_State *L;
|
||||||
|
union {
|
||||||
|
LUAI_MAXALIGN; /* ensure maximum alignment for buffer */
|
||||||
|
char b[LUAL_BUFFERSIZE]; /* initial buffer */
|
||||||
|
} init;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#define luaL_bufflen(bf) ((bf)->n)
|
||||||
|
#define luaL_buffaddr(bf) ((bf)->b)
|
||||||
|
|
||||||
|
|
||||||
|
#define luaL_addchar(B,c) \
|
||||||
|
((void)((B)->n < (B)->size || luaL_prepbuffsize((B), 1)), \
|
||||||
|
((B)->b[(B)->n++] = (c)))
|
||||||
|
|
||||||
|
#define luaL_addsize(B,s) ((B)->n += (s))
|
||||||
|
|
||||||
|
#define luaL_buffsub(B,s) ((B)->n -= (s))
|
||||||
|
|
||||||
|
LUALIB_API void (luaL_buffinit) (lua_State *L, luaL_Buffer *B);
|
||||||
|
LUALIB_API char *(luaL_prepbuffsize) (luaL_Buffer *B, size_t sz);
|
||||||
|
LUALIB_API void (luaL_addlstring) (luaL_Buffer *B, const char *s, size_t l);
|
||||||
|
LUALIB_API void (luaL_addstring) (luaL_Buffer *B, const char *s);
|
||||||
|
LUALIB_API void (luaL_addvalue) (luaL_Buffer *B);
|
||||||
|
LUALIB_API void (luaL_pushresult) (luaL_Buffer *B);
|
||||||
|
LUALIB_API void (luaL_pushresultsize) (luaL_Buffer *B, size_t sz);
|
||||||
|
LUALIB_API char *(luaL_buffinitsize) (lua_State *L, luaL_Buffer *B, size_t sz);
|
||||||
|
|
||||||
|
#define luaL_prepbuffer(B) luaL_prepbuffsize(B, LUAL_BUFFERSIZE)
|
||||||
|
|
||||||
|
/* }====================================================== */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** {======================================================
|
||||||
|
** File handles for IO library
|
||||||
|
** =======================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
** A file handle is a userdata with metatable 'LUA_FILEHANDLE' and
|
||||||
|
** initial structure 'luaL_Stream' (it may contain other fields
|
||||||
|
** after that initial structure).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define LUA_FILEHANDLE "FILE*"
|
||||||
|
|
||||||
|
|
||||||
|
typedef struct luaL_Stream {
|
||||||
|
FILE *f; /* stream (NULL for incompletely created streams) */
|
||||||
|
lua_CFunction closef; /* to close stream (NULL for closed streams) */
|
||||||
|
} luaL_Stream;
|
||||||
|
|
||||||
|
/* }====================================================== */
|
||||||
|
|
||||||
|
/*
|
||||||
|
** {==================================================================
|
||||||
|
** "Abstraction Layer" for basic report of messages and errors
|
||||||
|
** ===================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* print a string */
|
||||||
|
#if !defined(lua_writestring)
|
||||||
|
#define lua_writestring(s,l) fwrite((s), sizeof(char), (l), stdout)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* print a newline and flush the output */
|
||||||
|
#if !defined(lua_writeline)
|
||||||
|
#define lua_writeline() (lua_writestring("\n", 1), fflush(stdout))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* print an error message */
|
||||||
|
#if !defined(lua_writestringerror)
|
||||||
|
#define lua_writestringerror(s,p) \
|
||||||
|
(fprintf(stderr, (s), (p)), fflush(stderr))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* }================================================================== */
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** {============================================================
|
||||||
|
** Compatibility with deprecated conversions
|
||||||
|
** =============================================================
|
||||||
|
*/
|
||||||
|
#if defined(LUA_COMPAT_APIINTCASTS)
|
||||||
|
|
||||||
|
#define luaL_checkunsigned(L,a) ((lua_Unsigned)luaL_checkinteger(L,a))
|
||||||
|
#define luaL_optunsigned(L,a,d) \
|
||||||
|
((lua_Unsigned)luaL_optinteger(L,a,(lua_Integer)(d)))
|
||||||
|
|
||||||
|
#define luaL_checkint(L,n) ((int)luaL_checkinteger(L, (n)))
|
||||||
|
#define luaL_optint(L,n,d) ((int)luaL_optinteger(L, (n), (d)))
|
||||||
|
|
||||||
|
#define luaL_checklong(L,n) ((long)luaL_checkinteger(L, (n)))
|
||||||
|
#define luaL_optlong(L,n,d) ((long)luaL_optinteger(L, (n), (d)))
|
||||||
|
|
||||||
|
#endif
|
||||||
|
/* }============================================================ */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,549 @@
|
|||||||
|
/*
|
||||||
|
** $Id: lbaselib.c $
|
||||||
|
** Basic library
|
||||||
|
** See Copyright Notice in lua.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define lbaselib_c
|
||||||
|
#define LUA_LIB
|
||||||
|
|
||||||
|
#include "lprefix.h"
|
||||||
|
|
||||||
|
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "lua.h"
|
||||||
|
|
||||||
|
#include "lauxlib.h"
|
||||||
|
#include "lualib.h"
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_print (lua_State *L) {
|
||||||
|
int n = lua_gettop(L); /* number of arguments */
|
||||||
|
int i;
|
||||||
|
for (i = 1; i <= n; i++) { /* for each argument */
|
||||||
|
size_t l;
|
||||||
|
const char *s = luaL_tolstring(L, i, &l); /* convert it to string */
|
||||||
|
if (i > 1) /* not the first element? */
|
||||||
|
lua_writestring("\t", 1); /* add a tab before it */
|
||||||
|
lua_writestring(s, l); /* print it */
|
||||||
|
lua_pop(L, 1); /* pop result */
|
||||||
|
}
|
||||||
|
lua_writeline();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Creates a warning with all given arguments.
|
||||||
|
** Check first for errors; otherwise an error may interrupt
|
||||||
|
** the composition of a warning, leaving it unfinished.
|
||||||
|
*/
|
||||||
|
static int luaB_warn (lua_State *L) {
|
||||||
|
int n = lua_gettop(L); /* number of arguments */
|
||||||
|
int i;
|
||||||
|
luaL_checkstring(L, 1); /* at least one argument */
|
||||||
|
for (i = 2; i <= n; i++)
|
||||||
|
luaL_checkstring(L, i); /* make sure all arguments are strings */
|
||||||
|
for (i = 1; i < n; i++) /* compose warning */
|
||||||
|
lua_warning(L, lua_tostring(L, i), 1);
|
||||||
|
lua_warning(L, lua_tostring(L, n), 0); /* close warning */
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#define SPACECHARS " \f\n\r\t\v"
|
||||||
|
|
||||||
|
static const char *b_str2int (const char *s, int base, lua_Integer *pn) {
|
||||||
|
lua_Unsigned n = 0;
|
||||||
|
int neg = 0;
|
||||||
|
s += strspn(s, SPACECHARS); /* skip initial spaces */
|
||||||
|
if (*s == '-') { s++; neg = 1; } /* handle sign */
|
||||||
|
else if (*s == '+') s++;
|
||||||
|
if (!isalnum((unsigned char)*s)) /* no digit? */
|
||||||
|
return NULL;
|
||||||
|
do {
|
||||||
|
int digit = (isdigit((unsigned char)*s)) ? *s - '0'
|
||||||
|
: (toupper((unsigned char)*s) - 'A') + 10;
|
||||||
|
if (digit >= base) return NULL; /* invalid numeral */
|
||||||
|
n = n * base + digit;
|
||||||
|
s++;
|
||||||
|
} while (isalnum((unsigned char)*s));
|
||||||
|
s += strspn(s, SPACECHARS); /* skip trailing spaces */
|
||||||
|
*pn = (lua_Integer)((neg) ? (0u - n) : n);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_tonumber (lua_State *L) {
|
||||||
|
if (lua_isnoneornil(L, 2)) { /* standard conversion? */
|
||||||
|
if (lua_type(L, 1) == LUA_TNUMBER) { /* already a number? */
|
||||||
|
lua_settop(L, 1); /* yes; return it */
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
size_t l;
|
||||||
|
const char *s = lua_tolstring(L, 1, &l);
|
||||||
|
if (s != NULL && lua_stringtonumber(L, s) == l + 1)
|
||||||
|
return 1; /* successful conversion to number */
|
||||||
|
/* else not a number */
|
||||||
|
luaL_checkany(L, 1); /* (but there must be some parameter) */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
size_t l;
|
||||||
|
const char *s;
|
||||||
|
lua_Integer n = 0; /* to avoid warnings */
|
||||||
|
lua_Integer base = luaL_checkinteger(L, 2);
|
||||||
|
luaL_checktype(L, 1, LUA_TSTRING); /* no numbers as strings */
|
||||||
|
s = lua_tolstring(L, 1, &l);
|
||||||
|
luaL_argcheck(L, 2 <= base && base <= 36, 2, "base out of range");
|
||||||
|
if (b_str2int(s, (int)base, &n) == s + l) {
|
||||||
|
lua_pushinteger(L, n);
|
||||||
|
return 1;
|
||||||
|
} /* else not a number */
|
||||||
|
} /* else not a number */
|
||||||
|
luaL_pushfail(L); /* not a number */
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_error (lua_State *L) {
|
||||||
|
int level = (int)luaL_optinteger(L, 2, 1);
|
||||||
|
lua_settop(L, 1);
|
||||||
|
if (lua_type(L, 1) == LUA_TSTRING && level > 0) {
|
||||||
|
luaL_where(L, level); /* add extra information */
|
||||||
|
lua_pushvalue(L, 1);
|
||||||
|
lua_concat(L, 2);
|
||||||
|
}
|
||||||
|
return lua_error(L);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_getmetatable (lua_State *L) {
|
||||||
|
luaL_checkany(L, 1);
|
||||||
|
if (!lua_getmetatable(L, 1)) {
|
||||||
|
lua_pushnil(L);
|
||||||
|
return 1; /* no metatable */
|
||||||
|
}
|
||||||
|
luaL_getmetafield(L, 1, "__metatable");
|
||||||
|
return 1; /* returns either __metatable field (if present) or metatable */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_setmetatable (lua_State *L) {
|
||||||
|
int t = lua_type(L, 2);
|
||||||
|
luaL_checktype(L, 1, LUA_TTABLE);
|
||||||
|
luaL_argexpected(L, t == LUA_TNIL || t == LUA_TTABLE, 2, "nil or table");
|
||||||
|
if (l_unlikely(luaL_getmetafield(L, 1, "__metatable") != LUA_TNIL))
|
||||||
|
return luaL_error(L, "cannot change a protected metatable");
|
||||||
|
lua_settop(L, 2);
|
||||||
|
lua_setmetatable(L, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_rawequal (lua_State *L) {
|
||||||
|
luaL_checkany(L, 1);
|
||||||
|
luaL_checkany(L, 2);
|
||||||
|
lua_pushboolean(L, lua_rawequal(L, 1, 2));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_rawlen (lua_State *L) {
|
||||||
|
int t = lua_type(L, 1);
|
||||||
|
luaL_argexpected(L, t == LUA_TTABLE || t == LUA_TSTRING, 1,
|
||||||
|
"table or string");
|
||||||
|
lua_pushinteger(L, lua_rawlen(L, 1));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_rawget (lua_State *L) {
|
||||||
|
luaL_checktype(L, 1, LUA_TTABLE);
|
||||||
|
luaL_checkany(L, 2);
|
||||||
|
lua_settop(L, 2);
|
||||||
|
lua_rawget(L, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int luaB_rawset (lua_State *L) {
|
||||||
|
luaL_checktype(L, 1, LUA_TTABLE);
|
||||||
|
luaL_checkany(L, 2);
|
||||||
|
luaL_checkany(L, 3);
|
||||||
|
lua_settop(L, 3);
|
||||||
|
lua_rawset(L, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int pushmode (lua_State *L, int oldmode) {
|
||||||
|
if (oldmode == -1)
|
||||||
|
luaL_pushfail(L); /* invalid call to 'lua_gc' */
|
||||||
|
else
|
||||||
|
lua_pushstring(L, (oldmode == LUA_GCINC) ? "incremental"
|
||||||
|
: "generational");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** check whether call to 'lua_gc' was valid (not inside a finalizer)
|
||||||
|
*/
|
||||||
|
#define checkvalres(res) { if (res == -1) break; }
|
||||||
|
|
||||||
|
static int luaB_collectgarbage (lua_State *L) {
|
||||||
|
static const char *const opts[] = {"stop", "restart", "collect",
|
||||||
|
"count", "step", "setpause", "setstepmul",
|
||||||
|
"isrunning", "generational", "incremental", NULL};
|
||||||
|
static const int optsnum[] = {LUA_GCSTOP, LUA_GCRESTART, LUA_GCCOLLECT,
|
||||||
|
LUA_GCCOUNT, LUA_GCSTEP, LUA_GCSETPAUSE, LUA_GCSETSTEPMUL,
|
||||||
|
LUA_GCISRUNNING, LUA_GCGEN, LUA_GCINC};
|
||||||
|
int o = optsnum[luaL_checkoption(L, 1, "collect", opts)];
|
||||||
|
switch (o) {
|
||||||
|
case LUA_GCCOUNT: {
|
||||||
|
int k = lua_gc(L, o);
|
||||||
|
int b = lua_gc(L, LUA_GCCOUNTB);
|
||||||
|
checkvalres(k);
|
||||||
|
lua_pushnumber(L, (lua_Number)k + ((lua_Number)b/1024));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
case LUA_GCSTEP: {
|
||||||
|
int step = (int)luaL_optinteger(L, 2, 0);
|
||||||
|
int res = lua_gc(L, o, step);
|
||||||
|
checkvalres(res);
|
||||||
|
lua_pushboolean(L, res);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
case LUA_GCSETPAUSE:
|
||||||
|
case LUA_GCSETSTEPMUL: {
|
||||||
|
int p = (int)luaL_optinteger(L, 2, 0);
|
||||||
|
int previous = lua_gc(L, o, p);
|
||||||
|
checkvalres(previous);
|
||||||
|
lua_pushinteger(L, previous);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
case LUA_GCISRUNNING: {
|
||||||
|
int res = lua_gc(L, o);
|
||||||
|
checkvalres(res);
|
||||||
|
lua_pushboolean(L, res);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
case LUA_GCGEN: {
|
||||||
|
int minormul = (int)luaL_optinteger(L, 2, 0);
|
||||||
|
int majormul = (int)luaL_optinteger(L, 3, 0);
|
||||||
|
return pushmode(L, lua_gc(L, o, minormul, majormul));
|
||||||
|
}
|
||||||
|
case LUA_GCINC: {
|
||||||
|
int pause = (int)luaL_optinteger(L, 2, 0);
|
||||||
|
int stepmul = (int)luaL_optinteger(L, 3, 0);
|
||||||
|
int stepsize = (int)luaL_optinteger(L, 4, 0);
|
||||||
|
return pushmode(L, lua_gc(L, o, pause, stepmul, stepsize));
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
int res = lua_gc(L, o);
|
||||||
|
checkvalres(res);
|
||||||
|
lua_pushinteger(L, res);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
luaL_pushfail(L); /* invalid call (inside a finalizer) */
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_type (lua_State *L) {
|
||||||
|
int t = lua_type(L, 1);
|
||||||
|
luaL_argcheck(L, t != LUA_TNONE, 1, "value expected");
|
||||||
|
lua_pushstring(L, lua_typename(L, t));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_next (lua_State *L) {
|
||||||
|
luaL_checktype(L, 1, LUA_TTABLE);
|
||||||
|
lua_settop(L, 2); /* create a 2nd argument if there isn't one */
|
||||||
|
if (lua_next(L, 1))
|
||||||
|
return 2;
|
||||||
|
else {
|
||||||
|
lua_pushnil(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int pairscont (lua_State *L, int status, lua_KContext k) {
|
||||||
|
(void)L; (void)status; (void)k; /* unused */
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int luaB_pairs (lua_State *L) {
|
||||||
|
luaL_checkany(L, 1);
|
||||||
|
if (luaL_getmetafield(L, 1, "__pairs") == LUA_TNIL) { /* no metamethod? */
|
||||||
|
lua_pushcfunction(L, luaB_next); /* will return generator, */
|
||||||
|
lua_pushvalue(L, 1); /* state, */
|
||||||
|
lua_pushnil(L); /* and initial value */
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
lua_pushvalue(L, 1); /* argument 'self' to metamethod */
|
||||||
|
lua_callk(L, 1, 3, 0, pairscont); /* get 3 values from metamethod */
|
||||||
|
}
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Traversal function for 'ipairs'
|
||||||
|
*/
|
||||||
|
static int ipairsaux (lua_State *L) {
|
||||||
|
lua_Integer i = luaL_checkinteger(L, 2);
|
||||||
|
i = luaL_intop(+, i, 1);
|
||||||
|
lua_pushinteger(L, i);
|
||||||
|
return (lua_geti(L, 1, i) == LUA_TNIL) ? 1 : 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** 'ipairs' function. Returns 'ipairsaux', given "table", 0.
|
||||||
|
** (The given "table" may not be a table.)
|
||||||
|
*/
|
||||||
|
static int luaB_ipairs (lua_State *L) {
|
||||||
|
luaL_checkany(L, 1);
|
||||||
|
lua_pushcfunction(L, ipairsaux); /* iteration function */
|
||||||
|
lua_pushvalue(L, 1); /* state */
|
||||||
|
lua_pushinteger(L, 0); /* initial value */
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int load_aux (lua_State *L, int status, int envidx) {
|
||||||
|
if (l_likely(status == LUA_OK)) {
|
||||||
|
if (envidx != 0) { /* 'env' parameter? */
|
||||||
|
lua_pushvalue(L, envidx); /* environment for loaded function */
|
||||||
|
if (!lua_setupvalue(L, -2, 1)) /* set it as 1st upvalue */
|
||||||
|
lua_pop(L, 1); /* remove 'env' if not used by previous call */
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
else { /* error (message is on top of the stack) */
|
||||||
|
luaL_pushfail(L);
|
||||||
|
lua_insert(L, -2); /* put before error message */
|
||||||
|
return 2; /* return fail plus error message */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_loadfile (lua_State *L) {
|
||||||
|
const char *fname = luaL_optstring(L, 1, NULL);
|
||||||
|
const char *mode = luaL_optstring(L, 2, NULL);
|
||||||
|
int env = (!lua_isnone(L, 3) ? 3 : 0); /* 'env' index or 0 if no 'env' */
|
||||||
|
int status = luaL_loadfilex(L, fname, mode);
|
||||||
|
return load_aux(L, status, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** {======================================================
|
||||||
|
** Generic Read function
|
||||||
|
** =======================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** reserved slot, above all arguments, to hold a copy of the returned
|
||||||
|
** string to avoid it being collected while parsed. 'load' has four
|
||||||
|
** optional arguments (chunk, source name, mode, and environment).
|
||||||
|
*/
|
||||||
|
#define RESERVEDSLOT 5
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Reader for generic 'load' function: 'lua_load' uses the
|
||||||
|
** stack for internal stuff, so the reader cannot change the
|
||||||
|
** stack top. Instead, it keeps its resulting string in a
|
||||||
|
** reserved slot inside the stack.
|
||||||
|
*/
|
||||||
|
static const char *generic_reader (lua_State *L, void *ud, size_t *size) {
|
||||||
|
(void)(ud); /* not used */
|
||||||
|
luaL_checkstack(L, 2, "too many nested functions");
|
||||||
|
lua_pushvalue(L, 1); /* get function */
|
||||||
|
lua_call(L, 0, 1); /* call it */
|
||||||
|
if (lua_isnil(L, -1)) {
|
||||||
|
lua_pop(L, 1); /* pop result */
|
||||||
|
*size = 0;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
else if (l_unlikely(!lua_isstring(L, -1)))
|
||||||
|
luaL_error(L, "reader function must return a string");
|
||||||
|
lua_replace(L, RESERVEDSLOT); /* save string in reserved slot */
|
||||||
|
return lua_tolstring(L, RESERVEDSLOT, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_load (lua_State *L) {
|
||||||
|
int status;
|
||||||
|
size_t l;
|
||||||
|
const char *s = lua_tolstring(L, 1, &l);
|
||||||
|
const char *mode = luaL_optstring(L, 3, "bt");
|
||||||
|
int env = (!lua_isnone(L, 4) ? 4 : 0); /* 'env' index or 0 if no 'env' */
|
||||||
|
if (s != NULL) { /* loading a string? */
|
||||||
|
const char *chunkname = luaL_optstring(L, 2, s);
|
||||||
|
status = luaL_loadbufferx(L, s, l, chunkname, mode);
|
||||||
|
}
|
||||||
|
else { /* loading from a reader function */
|
||||||
|
const char *chunkname = luaL_optstring(L, 2, "=(load)");
|
||||||
|
luaL_checktype(L, 1, LUA_TFUNCTION);
|
||||||
|
lua_settop(L, RESERVEDSLOT); /* create reserved slot */
|
||||||
|
status = lua_load(L, generic_reader, NULL, chunkname, mode);
|
||||||
|
}
|
||||||
|
return load_aux(L, status, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* }====================================================== */
|
||||||
|
|
||||||
|
|
||||||
|
static int dofilecont (lua_State *L, int d1, lua_KContext d2) {
|
||||||
|
(void)d1; (void)d2; /* only to match 'lua_Kfunction' prototype */
|
||||||
|
return lua_gettop(L) - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_dofile (lua_State *L) {
|
||||||
|
const char *fname = luaL_optstring(L, 1, NULL);
|
||||||
|
lua_settop(L, 1);
|
||||||
|
if (l_unlikely(luaL_loadfile(L, fname) != LUA_OK))
|
||||||
|
return lua_error(L);
|
||||||
|
lua_callk(L, 0, LUA_MULTRET, 0, dofilecont);
|
||||||
|
return dofilecont(L, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_assert (lua_State *L) {
|
||||||
|
if (l_likely(lua_toboolean(L, 1))) /* condition is true? */
|
||||||
|
return lua_gettop(L); /* return all arguments */
|
||||||
|
else { /* error */
|
||||||
|
luaL_checkany(L, 1); /* there must be a condition */
|
||||||
|
lua_remove(L, 1); /* remove it */
|
||||||
|
lua_pushliteral(L, "assertion failed!"); /* default message */
|
||||||
|
lua_settop(L, 1); /* leave only message (default if no other one) */
|
||||||
|
return luaB_error(L); /* call 'error' */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_select (lua_State *L) {
|
||||||
|
int n = lua_gettop(L);
|
||||||
|
if (lua_type(L, 1) == LUA_TSTRING && *lua_tostring(L, 1) == '#') {
|
||||||
|
lua_pushinteger(L, n-1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
lua_Integer i = luaL_checkinteger(L, 1);
|
||||||
|
if (i < 0) i = n + i;
|
||||||
|
else if (i > n) i = n;
|
||||||
|
luaL_argcheck(L, 1 <= i, 1, "index out of range");
|
||||||
|
return n - (int)i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Continuation function for 'pcall' and 'xpcall'. Both functions
|
||||||
|
** already pushed a 'true' before doing the call, so in case of success
|
||||||
|
** 'finishpcall' only has to return everything in the stack minus
|
||||||
|
** 'extra' values (where 'extra' is exactly the number of items to be
|
||||||
|
** ignored).
|
||||||
|
*/
|
||||||
|
static int finishpcall (lua_State *L, int status, lua_KContext extra) {
|
||||||
|
if (l_unlikely(status != LUA_OK && status != LUA_YIELD)) { /* error? */
|
||||||
|
lua_pushboolean(L, 0); /* first result (false) */
|
||||||
|
lua_pushvalue(L, -2); /* error message */
|
||||||
|
return 2; /* return false, msg */
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return lua_gettop(L) - (int)extra; /* return all results */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_pcall (lua_State *L) {
|
||||||
|
int status;
|
||||||
|
luaL_checkany(L, 1);
|
||||||
|
lua_pushboolean(L, 1); /* first result if no errors */
|
||||||
|
lua_insert(L, 1); /* put it in place */
|
||||||
|
status = lua_pcallk(L, lua_gettop(L) - 2, LUA_MULTRET, 0, 0, finishpcall);
|
||||||
|
return finishpcall(L, status, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Do a protected call with error handling. After 'lua_rotate', the
|
||||||
|
** stack will have <f, err, true, f, [args...]>; so, the function passes
|
||||||
|
** 2 to 'finishpcall' to skip the 2 first values when returning results.
|
||||||
|
*/
|
||||||
|
static int luaB_xpcall (lua_State *L) {
|
||||||
|
int status;
|
||||||
|
int n = lua_gettop(L);
|
||||||
|
luaL_checktype(L, 2, LUA_TFUNCTION); /* check error function */
|
||||||
|
lua_pushboolean(L, 1); /* first result */
|
||||||
|
lua_pushvalue(L, 1); /* function */
|
||||||
|
lua_rotate(L, 3, 2); /* move them below function's arguments */
|
||||||
|
status = lua_pcallk(L, n - 2, LUA_MULTRET, 2, 2, finishpcall);
|
||||||
|
return finishpcall(L, status, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_tostring (lua_State *L) {
|
||||||
|
luaL_checkany(L, 1);
|
||||||
|
luaL_tolstring(L, 1, NULL);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static const luaL_Reg base_funcs[] = {
|
||||||
|
{"assert", luaB_assert},
|
||||||
|
{"collectgarbage", luaB_collectgarbage},
|
||||||
|
{"dofile", luaB_dofile},
|
||||||
|
{"error", luaB_error},
|
||||||
|
{"getmetatable", luaB_getmetatable},
|
||||||
|
{"ipairs", luaB_ipairs},
|
||||||
|
{"loadfile", luaB_loadfile},
|
||||||
|
{"load", luaB_load},
|
||||||
|
{"next", luaB_next},
|
||||||
|
{"pairs", luaB_pairs},
|
||||||
|
{"pcall", luaB_pcall},
|
||||||
|
{"print", luaB_print},
|
||||||
|
{"warn", luaB_warn},
|
||||||
|
{"rawequal", luaB_rawequal},
|
||||||
|
{"rawlen", luaB_rawlen},
|
||||||
|
{"rawget", luaB_rawget},
|
||||||
|
{"rawset", luaB_rawset},
|
||||||
|
{"select", luaB_select},
|
||||||
|
{"setmetatable", luaB_setmetatable},
|
||||||
|
{"tonumber", luaB_tonumber},
|
||||||
|
{"tostring", luaB_tostring},
|
||||||
|
{"type", luaB_type},
|
||||||
|
{"xpcall", luaB_xpcall},
|
||||||
|
/* placeholders */
|
||||||
|
{LUA_GNAME, NULL},
|
||||||
|
{"_VERSION", NULL},
|
||||||
|
{NULL, NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
LUAMOD_API int luaopen_base (lua_State *L) {
|
||||||
|
/* open lib into global table */
|
||||||
|
lua_pushglobaltable(L);
|
||||||
|
luaL_setfuncs(L, base_funcs, 0);
|
||||||
|
/* set global _G */
|
||||||
|
lua_pushvalue(L, -1);
|
||||||
|
lua_setfield(L, -2, LUA_GNAME);
|
||||||
|
/* set global _VERSION */
|
||||||
|
lua_pushliteral(L, LUA_VERSION);
|
||||||
|
lua_setfield(L, -2, "_VERSION");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
** $Id: lcode.h $
|
||||||
|
** Code generator for Lua
|
||||||
|
** See Copyright Notice in lua.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef lcode_h
|
||||||
|
#define lcode_h
|
||||||
|
|
||||||
|
#include "llex.h"
|
||||||
|
#include "lobject.h"
|
||||||
|
#include "lopcodes.h"
|
||||||
|
#include "lparser.h"
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Marks the end of a patch list. It is an invalid value both as an absolute
|
||||||
|
** address, and as a list link (would link an element to itself).
|
||||||
|
*/
|
||||||
|
#define NO_JUMP (-1)
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** grep "ORDER OPR" if you change these enums (ORDER OP)
|
||||||
|
*/
|
||||||
|
typedef enum BinOpr {
|
||||||
|
/* arithmetic operators */
|
||||||
|
OPR_ADD, OPR_SUB, OPR_MUL, OPR_MOD, OPR_POW,
|
||||||
|
OPR_DIV, OPR_IDIV,
|
||||||
|
/* bitwise operators */
|
||||||
|
OPR_BAND, OPR_BOR, OPR_BXOR,
|
||||||
|
OPR_SHL, OPR_SHR,
|
||||||
|
/* string operator */
|
||||||
|
OPR_CONCAT,
|
||||||
|
/* comparison operators */
|
||||||
|
OPR_EQ, OPR_LT, OPR_LE,
|
||||||
|
OPR_NE, OPR_GT, OPR_GE,
|
||||||
|
/* logical operators */
|
||||||
|
OPR_AND, OPR_OR,
|
||||||
|
OPR_NOBINOPR
|
||||||
|
} BinOpr;
|
||||||
|
|
||||||
|
|
||||||
|
/* true if operation is foldable (that is, it is arithmetic or bitwise) */
|
||||||
|
#define foldbinop(op) ((op) <= OPR_SHR)
|
||||||
|
|
||||||
|
|
||||||
|
#define luaK_codeABC(fs,o,a,b,c) luaK_codeABCk(fs,o,a,b,c,0)
|
||||||
|
|
||||||
|
|
||||||
|
typedef enum UnOpr { OPR_MINUS, OPR_BNOT, OPR_NOT, OPR_LEN, OPR_NOUNOPR } UnOpr;
|
||||||
|
|
||||||
|
|
||||||
|
/* get (pointer to) instruction of given 'expdesc' */
|
||||||
|
#define getinstruction(fs,e) ((fs)->f->code[(e)->u.info])
|
||||||
|
|
||||||
|
|
||||||
|
#define luaK_setmultret(fs,e) luaK_setreturns(fs, e, LUA_MULTRET)
|
||||||
|
|
||||||
|
#define luaK_jumpto(fs,t) luaK_patchlist(fs, luaK_jump(fs), t)
|
||||||
|
|
||||||
|
LUAI_FUNC int luaK_code (FuncState *fs, Instruction i);
|
||||||
|
LUAI_FUNC int luaK_codeABx (FuncState *fs, OpCode o, int A, unsigned int Bx);
|
||||||
|
LUAI_FUNC int luaK_codeABCk (FuncState *fs, OpCode o, int A,
|
||||||
|
int B, int C, int k);
|
||||||
|
LUAI_FUNC int luaK_exp2const (FuncState *fs, const expdesc *e, TValue *v);
|
||||||
|
LUAI_FUNC void luaK_fixline (FuncState *fs, int line);
|
||||||
|
LUAI_FUNC void luaK_nil (FuncState *fs, int from, int n);
|
||||||
|
LUAI_FUNC void luaK_reserveregs (FuncState *fs, int n);
|
||||||
|
LUAI_FUNC void luaK_checkstack (FuncState *fs, int n);
|
||||||
|
LUAI_FUNC void luaK_int (FuncState *fs, int reg, lua_Integer n);
|
||||||
|
LUAI_FUNC void luaK_dischargevars (FuncState *fs, expdesc *e);
|
||||||
|
LUAI_FUNC int luaK_exp2anyreg (FuncState *fs, expdesc *e);
|
||||||
|
LUAI_FUNC void luaK_exp2anyregup (FuncState *fs, expdesc *e);
|
||||||
|
LUAI_FUNC void luaK_exp2nextreg (FuncState *fs, expdesc *e);
|
||||||
|
LUAI_FUNC void luaK_exp2val (FuncState *fs, expdesc *e);
|
||||||
|
LUAI_FUNC void luaK_self (FuncState *fs, expdesc *e, expdesc *key);
|
||||||
|
LUAI_FUNC void luaK_indexed (FuncState *fs, expdesc *t, expdesc *k);
|
||||||
|
LUAI_FUNC void luaK_goiftrue (FuncState *fs, expdesc *e);
|
||||||
|
LUAI_FUNC void luaK_goiffalse (FuncState *fs, expdesc *e);
|
||||||
|
LUAI_FUNC void luaK_storevar (FuncState *fs, expdesc *var, expdesc *e);
|
||||||
|
LUAI_FUNC void luaK_setreturns (FuncState *fs, expdesc *e, int nresults);
|
||||||
|
LUAI_FUNC void luaK_setoneret (FuncState *fs, expdesc *e);
|
||||||
|
LUAI_FUNC int luaK_jump (FuncState *fs);
|
||||||
|
LUAI_FUNC void luaK_ret (FuncState *fs, int first, int nret);
|
||||||
|
LUAI_FUNC void luaK_patchlist (FuncState *fs, int list, int target);
|
||||||
|
LUAI_FUNC void luaK_patchtohere (FuncState *fs, int list);
|
||||||
|
LUAI_FUNC void luaK_concat (FuncState *fs, int *l1, int l2);
|
||||||
|
LUAI_FUNC int luaK_getlabel (FuncState *fs);
|
||||||
|
LUAI_FUNC void luaK_prefix (FuncState *fs, UnOpr op, expdesc *v, int line);
|
||||||
|
LUAI_FUNC void luaK_infix (FuncState *fs, BinOpr op, expdesc *v);
|
||||||
|
LUAI_FUNC void luaK_posfix (FuncState *fs, BinOpr op, expdesc *v1,
|
||||||
|
expdesc *v2, int line);
|
||||||
|
LUAI_FUNC void luaK_settablesize (FuncState *fs, int pc,
|
||||||
|
int ra, int asize, int hsize);
|
||||||
|
LUAI_FUNC void luaK_setlist (FuncState *fs, int base, int nelems, int tostore);
|
||||||
|
LUAI_FUNC void luaK_finish (FuncState *fs);
|
||||||
|
LUAI_FUNC l_noret luaK_semerror (LexState *ls, const char *msg);
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
/*
|
||||||
|
** $Id: lcorolib.c $
|
||||||
|
** Coroutine Library
|
||||||
|
** See Copyright Notice in lua.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define lcorolib_c
|
||||||
|
#define LUA_LIB
|
||||||
|
|
||||||
|
#include "lprefix.h"
|
||||||
|
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include "lua.h"
|
||||||
|
|
||||||
|
#include "lauxlib.h"
|
||||||
|
#include "lualib.h"
|
||||||
|
|
||||||
|
|
||||||
|
static lua_State *getco (lua_State *L) {
|
||||||
|
lua_State *co = lua_tothread(L, 1);
|
||||||
|
luaL_argexpected(L, co, 1, "thread");
|
||||||
|
return co;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Resumes a coroutine. Returns the number of results for non-error
|
||||||
|
** cases or -1 for errors.
|
||||||
|
*/
|
||||||
|
static int auxresume (lua_State *L, lua_State *co, int narg) {
|
||||||
|
int status, nres;
|
||||||
|
if (l_unlikely(!lua_checkstack(co, narg))) {
|
||||||
|
lua_pushliteral(L, "too many arguments to resume");
|
||||||
|
return -1; /* error flag */
|
||||||
|
}
|
||||||
|
lua_xmove(L, co, narg);
|
||||||
|
status = lua_resume(co, L, narg, &nres);
|
||||||
|
if (l_likely(status == LUA_OK || status == LUA_YIELD)) {
|
||||||
|
if (l_unlikely(!lua_checkstack(L, nres + 1))) {
|
||||||
|
lua_pop(co, nres); /* remove results anyway */
|
||||||
|
lua_pushliteral(L, "too many results to resume");
|
||||||
|
return -1; /* error flag */
|
||||||
|
}
|
||||||
|
lua_xmove(co, L, nres); /* move yielded values */
|
||||||
|
return nres;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
lua_xmove(co, L, 1); /* move error message */
|
||||||
|
return -1; /* error flag */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_coresume (lua_State *L) {
|
||||||
|
lua_State *co = getco(L);
|
||||||
|
int r;
|
||||||
|
r = auxresume(L, co, lua_gettop(L) - 1);
|
||||||
|
if (l_unlikely(r < 0)) {
|
||||||
|
lua_pushboolean(L, 0);
|
||||||
|
lua_insert(L, -2);
|
||||||
|
return 2; /* return false + error message */
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
lua_pushboolean(L, 1);
|
||||||
|
lua_insert(L, -(r + 1));
|
||||||
|
return r + 1; /* return true + 'resume' returns */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_auxwrap (lua_State *L) {
|
||||||
|
lua_State *co = lua_tothread(L, lua_upvalueindex(1));
|
||||||
|
int r = auxresume(L, co, lua_gettop(L));
|
||||||
|
if (l_unlikely(r < 0)) { /* error? */
|
||||||
|
int stat = lua_status(co);
|
||||||
|
if (stat != LUA_OK && stat != LUA_YIELD) { /* error in the coroutine? */
|
||||||
|
stat = lua_closethread(co, L); /* close its tbc variables */
|
||||||
|
lua_assert(stat != LUA_OK);
|
||||||
|
lua_xmove(co, L, 1); /* move error message to the caller */
|
||||||
|
}
|
||||||
|
if (stat != LUA_ERRMEM && /* not a memory error and ... */
|
||||||
|
lua_type(L, -1) == LUA_TSTRING) { /* ... error object is a string? */
|
||||||
|
luaL_where(L, 1); /* add extra info, if available */
|
||||||
|
lua_insert(L, -2);
|
||||||
|
lua_concat(L, 2);
|
||||||
|
}
|
||||||
|
return lua_error(L); /* propagate error */
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_cocreate (lua_State *L) {
|
||||||
|
lua_State *NL;
|
||||||
|
luaL_checktype(L, 1, LUA_TFUNCTION);
|
||||||
|
NL = lua_newthread(L);
|
||||||
|
lua_pushvalue(L, 1); /* move function to top */
|
||||||
|
lua_xmove(L, NL, 1); /* move function from L to NL */
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_cowrap (lua_State *L) {
|
||||||
|
luaB_cocreate(L);
|
||||||
|
lua_pushcclosure(L, luaB_auxwrap, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_yield (lua_State *L) {
|
||||||
|
return lua_yield(L, lua_gettop(L));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#define COS_RUN 0
|
||||||
|
#define COS_DEAD 1
|
||||||
|
#define COS_YIELD 2
|
||||||
|
#define COS_NORM 3
|
||||||
|
|
||||||
|
|
||||||
|
static const char *const statname[] =
|
||||||
|
{"running", "dead", "suspended", "normal"};
|
||||||
|
|
||||||
|
|
||||||
|
static int auxstatus (lua_State *L, lua_State *co) {
|
||||||
|
if (L == co) return COS_RUN;
|
||||||
|
else {
|
||||||
|
switch (lua_status(co)) {
|
||||||
|
case LUA_YIELD:
|
||||||
|
return COS_YIELD;
|
||||||
|
case LUA_OK: {
|
||||||
|
lua_Debug ar;
|
||||||
|
if (lua_getstack(co, 0, &ar)) /* does it have frames? */
|
||||||
|
return COS_NORM; /* it is running */
|
||||||
|
else if (lua_gettop(co) == 0)
|
||||||
|
return COS_DEAD;
|
||||||
|
else
|
||||||
|
return COS_YIELD; /* initial state */
|
||||||
|
}
|
||||||
|
default: /* some error occurred */
|
||||||
|
return COS_DEAD;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_costatus (lua_State *L) {
|
||||||
|
lua_State *co = getco(L);
|
||||||
|
lua_pushstring(L, statname[auxstatus(L, co)]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_yieldable (lua_State *L) {
|
||||||
|
lua_State *co = lua_isnone(L, 1) ? L : getco(L);
|
||||||
|
lua_pushboolean(L, lua_isyieldable(co));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_corunning (lua_State *L) {
|
||||||
|
int ismain = lua_pushthread(L);
|
||||||
|
lua_pushboolean(L, ismain);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int luaB_close (lua_State *L) {
|
||||||
|
lua_State *co = getco(L);
|
||||||
|
int status = auxstatus(L, co);
|
||||||
|
switch (status) {
|
||||||
|
case COS_DEAD: case COS_YIELD: {
|
||||||
|
status = lua_closethread(co, L);
|
||||||
|
if (status == LUA_OK) {
|
||||||
|
lua_pushboolean(L, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
lua_pushboolean(L, 0);
|
||||||
|
lua_xmove(co, L, 1); /* move error message */
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default: /* normal or running coroutine */
|
||||||
|
return luaL_error(L, "cannot close a %s coroutine", statname[status]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static const luaL_Reg co_funcs[] = {
|
||||||
|
{"create", luaB_cocreate},
|
||||||
|
{"resume", luaB_coresume},
|
||||||
|
{"running", luaB_corunning},
|
||||||
|
{"status", luaB_costatus},
|
||||||
|
{"wrap", luaB_cowrap},
|
||||||
|
{"yield", luaB_yield},
|
||||||
|
{"isyieldable", luaB_yieldable},
|
||||||
|
{"close", luaB_close},
|
||||||
|
{NULL, NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
LUAMOD_API int luaopen_coroutine (lua_State *L) {
|
||||||
|
luaL_newlib(L, co_funcs);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
** $Id: lctype.c $
|
||||||
|
** 'ctype' functions for Lua
|
||||||
|
** See Copyright Notice in lua.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define lctype_c
|
||||||
|
#define LUA_CORE
|
||||||
|
|
||||||
|
#include "lprefix.h"
|
||||||
|
|
||||||
|
|
||||||
|
#include "lctype.h"
|
||||||
|
|
||||||
|
#if !LUA_USE_CTYPE /* { */
|
||||||
|
|
||||||
|
#include <limits.h>
|
||||||
|
|
||||||
|
|
||||||
|
#if defined (LUA_UCID) /* accept UniCode IDentifiers? */
|
||||||
|
/* consider all non-ascii codepoints to be alphabetic */
|
||||||
|
#define NONA 0x01
|
||||||
|
#else
|
||||||
|
#define NONA 0x00 /* default */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
LUAI_DDEF const lu_byte luai_ctype_[UCHAR_MAX + 2] = {
|
||||||
|
0x00, /* EOZ */
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 0. */
|
||||||
|
0x00, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 1. */
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x0c, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, /* 2. */
|
||||||
|
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
|
||||||
|
0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, /* 3. */
|
||||||
|
0x16, 0x16, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
|
||||||
|
0x04, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x05, /* 4. */
|
||||||
|
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
|
||||||
|
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, /* 5. */
|
||||||
|
0x05, 0x05, 0x05, 0x04, 0x04, 0x04, 0x04, 0x05,
|
||||||
|
0x04, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x05, /* 6. */
|
||||||
|
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
|
||||||
|
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, /* 7. */
|
||||||
|
0x05, 0x05, 0x05, 0x04, 0x04, 0x04, 0x04, 0x00,
|
||||||
|
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, /* 8. */
|
||||||
|
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA,
|
||||||
|
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, /* 9. */
|
||||||
|
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA,
|
||||||
|
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, /* a. */
|
||||||
|
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA,
|
||||||
|
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, /* b. */
|
||||||
|
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA,
|
||||||
|
0x00, 0x00, NONA, NONA, NONA, NONA, NONA, NONA, /* c. */
|
||||||
|
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA,
|
||||||
|
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, /* d. */
|
||||||
|
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA,
|
||||||
|
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, /* e. */
|
||||||
|
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA,
|
||||||
|
NONA, NONA, NONA, NONA, NONA, 0x00, 0x00, 0x00, /* f. */
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* } */
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
** $Id: lctype.h $
|
||||||
|
** 'ctype' functions for Lua
|
||||||
|
** See Copyright Notice in lua.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef lctype_h
|
||||||
|
#define lctype_h
|
||||||
|
|
||||||
|
#include "lua.h"
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** WARNING: the functions defined here do not necessarily correspond
|
||||||
|
** to the similar functions in the standard C ctype.h. They are
|
||||||
|
** optimized for the specific needs of Lua.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#if !defined(LUA_USE_CTYPE)
|
||||||
|
|
||||||
|
#if 'A' == 65 && '0' == 48
|
||||||
|
/* ASCII case: can use its own tables; faster and fixed */
|
||||||
|
#define LUA_USE_CTYPE 0
|
||||||
|
#else
|
||||||
|
/* must use standard C ctype */
|
||||||
|
#define LUA_USE_CTYPE 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
#if !LUA_USE_CTYPE /* { */
|
||||||
|
|
||||||
|
#include <limits.h>
|
||||||
|
|
||||||
|
#include "llimits.h"
|
||||||
|
|
||||||
|
|
||||||
|
#define ALPHABIT 0
|
||||||
|
#define DIGITBIT 1
|
||||||
|
#define PRINTBIT 2
|
||||||
|
#define SPACEBIT 3
|
||||||
|
#define XDIGITBIT 4
|
||||||
|
|
||||||
|
|
||||||
|
#define MASK(B) (1 << (B))
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** add 1 to char to allow index -1 (EOZ)
|
||||||
|
*/
|
||||||
|
#define testprop(c,p) (luai_ctype_[(c)+1] & (p))
|
||||||
|
|
||||||
|
/*
|
||||||
|
** 'lalpha' (Lua alphabetic) and 'lalnum' (Lua alphanumeric) both include '_'
|
||||||
|
*/
|
||||||
|
#define lislalpha(c) testprop(c, MASK(ALPHABIT))
|
||||||
|
#define lislalnum(c) testprop(c, (MASK(ALPHABIT) | MASK(DIGITBIT)))
|
||||||
|
#define lisdigit(c) testprop(c, MASK(DIGITBIT))
|
||||||
|
#define lisspace(c) testprop(c, MASK(SPACEBIT))
|
||||||
|
#define lisprint(c) testprop(c, MASK(PRINTBIT))
|
||||||
|
#define lisxdigit(c) testprop(c, MASK(XDIGITBIT))
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** In ASCII, this 'ltolower' is correct for alphabetic characters and
|
||||||
|
** for '.'. That is enough for Lua needs. ('check_exp' ensures that
|
||||||
|
** the character either is an upper-case letter or is unchanged by
|
||||||
|
** the transformation, which holds for lower-case letters and '.'.)
|
||||||
|
*/
|
||||||
|
#define ltolower(c) \
|
||||||
|
check_exp(('A' <= (c) && (c) <= 'Z') || (c) == ((c) | ('A' ^ 'a')), \
|
||||||
|
(c) | ('A' ^ 'a'))
|
||||||
|
|
||||||
|
|
||||||
|
/* one entry for each character and for -1 (EOZ) */
|
||||||
|
LUAI_DDEC(const lu_byte luai_ctype_[UCHAR_MAX + 2];)
|
||||||
|
|
||||||
|
|
||||||
|
#else /* }{ */
|
||||||
|
|
||||||
|
/*
|
||||||
|
** use standard C ctypes
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <ctype.h>
|
||||||
|
|
||||||
|
|
||||||
|
#define lislalpha(c) (isalpha(c) || (c) == '_')
|
||||||
|
#define lislalnum(c) (isalnum(c) || (c) == '_')
|
||||||
|
#define lisdigit(c) (isdigit(c))
|
||||||
|
#define lisspace(c) (isspace(c))
|
||||||
|
#define lisprint(c) (isprint(c))
|
||||||
|
#define lisxdigit(c) (isxdigit(c))
|
||||||
|
|
||||||
|
#define ltolower(c) (tolower(c))
|
||||||
|
|
||||||
|
#endif /* } */
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
@@ -0,0 +1,483 @@
|
|||||||
|
/*
|
||||||
|
** $Id: ldblib.c $
|
||||||
|
** Interface from Lua to its debug API
|
||||||
|
** See Copyright Notice in lua.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define ldblib_c
|
||||||
|
#define LUA_LIB
|
||||||
|
|
||||||
|
#include "lprefix.h"
|
||||||
|
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "lua.h"
|
||||||
|
|
||||||
|
#include "lauxlib.h"
|
||||||
|
#include "lualib.h"
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** The hook table at registry[HOOKKEY] maps threads to their current
|
||||||
|
** hook function.
|
||||||
|
*/
|
||||||
|
static const char *const HOOKKEY = "_HOOKKEY";
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** If L1 != L, L1 can be in any state, and therefore there are no
|
||||||
|
** guarantees about its stack space; any push in L1 must be
|
||||||
|
** checked.
|
||||||
|
*/
|
||||||
|
static void checkstack (lua_State *L, lua_State *L1, int n) {
|
||||||
|
if (l_unlikely(L != L1 && !lua_checkstack(L1, n)))
|
||||||
|
luaL_error(L, "stack overflow");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int db_getregistry (lua_State *L) {
|
||||||
|
lua_pushvalue(L, LUA_REGISTRYINDEX);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int db_getmetatable (lua_State *L) {
|
||||||
|
luaL_checkany(L, 1);
|
||||||
|
if (!lua_getmetatable(L, 1)) {
|
||||||
|
lua_pushnil(L); /* no metatable */
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int db_setmetatable (lua_State *L) {
|
||||||
|
int t = lua_type(L, 2);
|
||||||
|
luaL_argexpected(L, t == LUA_TNIL || t == LUA_TTABLE, 2, "nil or table");
|
||||||
|
lua_settop(L, 2);
|
||||||
|
lua_setmetatable(L, 1);
|
||||||
|
return 1; /* return 1st argument */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int db_getuservalue (lua_State *L) {
|
||||||
|
int n = (int)luaL_optinteger(L, 2, 1);
|
||||||
|
if (lua_type(L, 1) != LUA_TUSERDATA)
|
||||||
|
luaL_pushfail(L);
|
||||||
|
else if (lua_getiuservalue(L, 1, n) != LUA_TNONE) {
|
||||||
|
lua_pushboolean(L, 1);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int db_setuservalue (lua_State *L) {
|
||||||
|
int n = (int)luaL_optinteger(L, 3, 1);
|
||||||
|
luaL_checktype(L, 1, LUA_TUSERDATA);
|
||||||
|
luaL_checkany(L, 2);
|
||||||
|
lua_settop(L, 2);
|
||||||
|
if (!lua_setiuservalue(L, 1, n))
|
||||||
|
luaL_pushfail(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Auxiliary function used by several library functions: check for
|
||||||
|
** an optional thread as function's first argument and set 'arg' with
|
||||||
|
** 1 if this argument is present (so that functions can skip it to
|
||||||
|
** access their other arguments)
|
||||||
|
*/
|
||||||
|
static lua_State *getthread (lua_State *L, int *arg) {
|
||||||
|
if (lua_isthread(L, 1)) {
|
||||||
|
*arg = 1;
|
||||||
|
return lua_tothread(L, 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
*arg = 0;
|
||||||
|
return L; /* function will operate over current thread */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Variations of 'lua_settable', used by 'db_getinfo' to put results
|
||||||
|
** from 'lua_getinfo' into result table. Key is always a string;
|
||||||
|
** value can be a string, an int, or a boolean.
|
||||||
|
*/
|
||||||
|
static void settabss (lua_State *L, const char *k, const char *v) {
|
||||||
|
lua_pushstring(L, v);
|
||||||
|
lua_setfield(L, -2, k);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void settabsi (lua_State *L, const char *k, int v) {
|
||||||
|
lua_pushinteger(L, v);
|
||||||
|
lua_setfield(L, -2, k);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void settabsb (lua_State *L, const char *k, int v) {
|
||||||
|
lua_pushboolean(L, v);
|
||||||
|
lua_setfield(L, -2, k);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** In function 'db_getinfo', the call to 'lua_getinfo' may push
|
||||||
|
** results on the stack; later it creates the result table to put
|
||||||
|
** these objects. Function 'treatstackoption' puts the result from
|
||||||
|
** 'lua_getinfo' on top of the result table so that it can call
|
||||||
|
** 'lua_setfield'.
|
||||||
|
*/
|
||||||
|
static void treatstackoption (lua_State *L, lua_State *L1, const char *fname) {
|
||||||
|
if (L == L1)
|
||||||
|
lua_rotate(L, -2, 1); /* exchange object and table */
|
||||||
|
else
|
||||||
|
lua_xmove(L1, L, 1); /* move object to the "main" stack */
|
||||||
|
lua_setfield(L, -2, fname); /* put object into table */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Calls 'lua_getinfo' and collects all results in a new table.
|
||||||
|
** L1 needs stack space for an optional input (function) plus
|
||||||
|
** two optional outputs (function and line table) from function
|
||||||
|
** 'lua_getinfo'.
|
||||||
|
*/
|
||||||
|
static int db_getinfo (lua_State *L) {
|
||||||
|
lua_Debug ar;
|
||||||
|
int arg;
|
||||||
|
lua_State *L1 = getthread(L, &arg);
|
||||||
|
const char *options = luaL_optstring(L, arg+2, "flnSrtu");
|
||||||
|
checkstack(L, L1, 3);
|
||||||
|
luaL_argcheck(L, options[0] != '>', arg + 2, "invalid option '>'");
|
||||||
|
if (lua_isfunction(L, arg + 1)) { /* info about a function? */
|
||||||
|
options = lua_pushfstring(L, ">%s", options); /* add '>' to 'options' */
|
||||||
|
lua_pushvalue(L, arg + 1); /* move function to 'L1' stack */
|
||||||
|
lua_xmove(L, L1, 1);
|
||||||
|
}
|
||||||
|
else { /* stack level */
|
||||||
|
if (!lua_getstack(L1, (int)luaL_checkinteger(L, arg + 1), &ar)) {
|
||||||
|
luaL_pushfail(L); /* level out of range */
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!lua_getinfo(L1, options, &ar))
|
||||||
|
return luaL_argerror(L, arg+2, "invalid option");
|
||||||
|
lua_newtable(L); /* table to collect results */
|
||||||
|
if (strchr(options, 'S')) {
|
||||||
|
lua_pushlstring(L, ar.source, ar.srclen);
|
||||||
|
lua_setfield(L, -2, "source");
|
||||||
|
settabss(L, "short_src", ar.short_src);
|
||||||
|
settabsi(L, "linedefined", ar.linedefined);
|
||||||
|
settabsi(L, "lastlinedefined", ar.lastlinedefined);
|
||||||
|
settabss(L, "what", ar.what);
|
||||||
|
}
|
||||||
|
if (strchr(options, 'l'))
|
||||||
|
settabsi(L, "currentline", ar.currentline);
|
||||||
|
if (strchr(options, 'u')) {
|
||||||
|
settabsi(L, "nups", ar.nups);
|
||||||
|
settabsi(L, "nparams", ar.nparams);
|
||||||
|
settabsb(L, "isvararg", ar.isvararg);
|
||||||
|
}
|
||||||
|
if (strchr(options, 'n')) {
|
||||||
|
settabss(L, "name", ar.name);
|
||||||
|
settabss(L, "namewhat", ar.namewhat);
|
||||||
|
}
|
||||||
|
if (strchr(options, 'r')) {
|
||||||
|
settabsi(L, "ftransfer", ar.ftransfer);
|
||||||
|
settabsi(L, "ntransfer", ar.ntransfer);
|
||||||
|
}
|
||||||
|
if (strchr(options, 't'))
|
||||||
|
settabsb(L, "istailcall", ar.istailcall);
|
||||||
|
if (strchr(options, 'L'))
|
||||||
|
treatstackoption(L, L1, "activelines");
|
||||||
|
if (strchr(options, 'f'))
|
||||||
|
treatstackoption(L, L1, "func");
|
||||||
|
return 1; /* return table */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int db_getlocal (lua_State *L) {
|
||||||
|
int arg;
|
||||||
|
lua_State *L1 = getthread(L, &arg);
|
||||||
|
int nvar = (int)luaL_checkinteger(L, arg + 2); /* local-variable index */
|
||||||
|
if (lua_isfunction(L, arg + 1)) { /* function argument? */
|
||||||
|
lua_pushvalue(L, arg + 1); /* push function */
|
||||||
|
lua_pushstring(L, lua_getlocal(L, NULL, nvar)); /* push local name */
|
||||||
|
return 1; /* return only name (there is no value) */
|
||||||
|
}
|
||||||
|
else { /* stack-level argument */
|
||||||
|
lua_Debug ar;
|
||||||
|
const char *name;
|
||||||
|
int level = (int)luaL_checkinteger(L, arg + 1);
|
||||||
|
if (l_unlikely(!lua_getstack(L1, level, &ar))) /* out of range? */
|
||||||
|
return luaL_argerror(L, arg+1, "level out of range");
|
||||||
|
checkstack(L, L1, 1);
|
||||||
|
name = lua_getlocal(L1, &ar, nvar);
|
||||||
|
if (name) {
|
||||||
|
lua_xmove(L1, L, 1); /* move local value */
|
||||||
|
lua_pushstring(L, name); /* push name */
|
||||||
|
lua_rotate(L, -2, 1); /* re-order */
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
luaL_pushfail(L); /* no name (nor value) */
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int db_setlocal (lua_State *L) {
|
||||||
|
int arg;
|
||||||
|
const char *name;
|
||||||
|
lua_State *L1 = getthread(L, &arg);
|
||||||
|
lua_Debug ar;
|
||||||
|
int level = (int)luaL_checkinteger(L, arg + 1);
|
||||||
|
int nvar = (int)luaL_checkinteger(L, arg + 2);
|
||||||
|
if (l_unlikely(!lua_getstack(L1, level, &ar))) /* out of range? */
|
||||||
|
return luaL_argerror(L, arg+1, "level out of range");
|
||||||
|
luaL_checkany(L, arg+3);
|
||||||
|
lua_settop(L, arg+3);
|
||||||
|
checkstack(L, L1, 1);
|
||||||
|
lua_xmove(L, L1, 1);
|
||||||
|
name = lua_setlocal(L1, &ar, nvar);
|
||||||
|
if (name == NULL)
|
||||||
|
lua_pop(L1, 1); /* pop value (if not popped by 'lua_setlocal') */
|
||||||
|
lua_pushstring(L, name);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** get (if 'get' is true) or set an upvalue from a closure
|
||||||
|
*/
|
||||||
|
static int auxupvalue (lua_State *L, int get) {
|
||||||
|
const char *name;
|
||||||
|
int n = (int)luaL_checkinteger(L, 2); /* upvalue index */
|
||||||
|
luaL_checktype(L, 1, LUA_TFUNCTION); /* closure */
|
||||||
|
name = get ? lua_getupvalue(L, 1, n) : lua_setupvalue(L, 1, n);
|
||||||
|
if (name == NULL) return 0;
|
||||||
|
lua_pushstring(L, name);
|
||||||
|
lua_insert(L, -(get+1)); /* no-op if get is false */
|
||||||
|
return get + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int db_getupvalue (lua_State *L) {
|
||||||
|
return auxupvalue(L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int db_setupvalue (lua_State *L) {
|
||||||
|
luaL_checkany(L, 3);
|
||||||
|
return auxupvalue(L, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Check whether a given upvalue from a given closure exists and
|
||||||
|
** returns its index
|
||||||
|
*/
|
||||||
|
static void *checkupval (lua_State *L, int argf, int argnup, int *pnup) {
|
||||||
|
void *id;
|
||||||
|
int nup = (int)luaL_checkinteger(L, argnup); /* upvalue index */
|
||||||
|
luaL_checktype(L, argf, LUA_TFUNCTION); /* closure */
|
||||||
|
id = lua_upvalueid(L, argf, nup);
|
||||||
|
if (pnup) {
|
||||||
|
luaL_argcheck(L, id != NULL, argnup, "invalid upvalue index");
|
||||||
|
*pnup = nup;
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int db_upvalueid (lua_State *L) {
|
||||||
|
void *id = checkupval(L, 1, 2, NULL);
|
||||||
|
if (id != NULL)
|
||||||
|
lua_pushlightuserdata(L, id);
|
||||||
|
else
|
||||||
|
luaL_pushfail(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int db_upvaluejoin (lua_State *L) {
|
||||||
|
int n1, n2;
|
||||||
|
checkupval(L, 1, 2, &n1);
|
||||||
|
checkupval(L, 3, 4, &n2);
|
||||||
|
luaL_argcheck(L, !lua_iscfunction(L, 1), 1, "Lua function expected");
|
||||||
|
luaL_argcheck(L, !lua_iscfunction(L, 3), 3, "Lua function expected");
|
||||||
|
lua_upvaluejoin(L, 1, n1, 3, n2);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Call hook function registered at hook table for the current
|
||||||
|
** thread (if there is one)
|
||||||
|
*/
|
||||||
|
static void hookf (lua_State *L, lua_Debug *ar) {
|
||||||
|
static const char *const hooknames[] =
|
||||||
|
{"call", "return", "line", "count", "tail call"};
|
||||||
|
lua_getfield(L, LUA_REGISTRYINDEX, HOOKKEY);
|
||||||
|
lua_pushthread(L);
|
||||||
|
if (lua_rawget(L, -2) == LUA_TFUNCTION) { /* is there a hook function? */
|
||||||
|
lua_pushstring(L, hooknames[(int)ar->event]); /* push event name */
|
||||||
|
if (ar->currentline >= 0)
|
||||||
|
lua_pushinteger(L, ar->currentline); /* push current line */
|
||||||
|
else lua_pushnil(L);
|
||||||
|
lua_assert(lua_getinfo(L, "lS", ar));
|
||||||
|
lua_call(L, 2, 0); /* call hook function */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Convert a string mask (for 'sethook') into a bit mask
|
||||||
|
*/
|
||||||
|
static int makemask (const char *smask, int count) {
|
||||||
|
int mask = 0;
|
||||||
|
if (strchr(smask, 'c')) mask |= LUA_MASKCALL;
|
||||||
|
if (strchr(smask, 'r')) mask |= LUA_MASKRET;
|
||||||
|
if (strchr(smask, 'l')) mask |= LUA_MASKLINE;
|
||||||
|
if (count > 0) mask |= LUA_MASKCOUNT;
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Convert a bit mask (for 'gethook') into a string mask
|
||||||
|
*/
|
||||||
|
static char *unmakemask (int mask, char *smask) {
|
||||||
|
int i = 0;
|
||||||
|
if (mask & LUA_MASKCALL) smask[i++] = 'c';
|
||||||
|
if (mask & LUA_MASKRET) smask[i++] = 'r';
|
||||||
|
if (mask & LUA_MASKLINE) smask[i++] = 'l';
|
||||||
|
smask[i] = '\0';
|
||||||
|
return smask;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int db_sethook (lua_State *L) {
|
||||||
|
int arg, mask, count;
|
||||||
|
lua_Hook func;
|
||||||
|
lua_State *L1 = getthread(L, &arg);
|
||||||
|
if (lua_isnoneornil(L, arg+1)) { /* no hook? */
|
||||||
|
lua_settop(L, arg+1);
|
||||||
|
func = NULL; mask = 0; count = 0; /* turn off hooks */
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const char *smask = luaL_checkstring(L, arg+2);
|
||||||
|
luaL_checktype(L, arg+1, LUA_TFUNCTION);
|
||||||
|
count = (int)luaL_optinteger(L, arg + 3, 0);
|
||||||
|
func = hookf; mask = makemask(smask, count);
|
||||||
|
}
|
||||||
|
if (!luaL_getsubtable(L, LUA_REGISTRYINDEX, HOOKKEY)) {
|
||||||
|
/* table just created; initialize it */
|
||||||
|
lua_pushliteral(L, "k");
|
||||||
|
lua_setfield(L, -2, "__mode"); /** hooktable.__mode = "k" */
|
||||||
|
lua_pushvalue(L, -1);
|
||||||
|
lua_setmetatable(L, -2); /* metatable(hooktable) = hooktable */
|
||||||
|
}
|
||||||
|
checkstack(L, L1, 1);
|
||||||
|
lua_pushthread(L1); lua_xmove(L1, L, 1); /* key (thread) */
|
||||||
|
lua_pushvalue(L, arg + 1); /* value (hook function) */
|
||||||
|
lua_rawset(L, -3); /* hooktable[L1] = new Lua hook */
|
||||||
|
lua_sethook(L1, func, mask, count);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int db_gethook (lua_State *L) {
|
||||||
|
int arg;
|
||||||
|
lua_State *L1 = getthread(L, &arg);
|
||||||
|
char buff[5];
|
||||||
|
int mask = lua_gethookmask(L1);
|
||||||
|
lua_Hook hook = lua_gethook(L1);
|
||||||
|
if (hook == NULL) { /* no hook? */
|
||||||
|
luaL_pushfail(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
else if (hook != hookf) /* external hook? */
|
||||||
|
lua_pushliteral(L, "external hook");
|
||||||
|
else { /* hook table must exist */
|
||||||
|
lua_getfield(L, LUA_REGISTRYINDEX, HOOKKEY);
|
||||||
|
checkstack(L, L1, 1);
|
||||||
|
lua_pushthread(L1); lua_xmove(L1, L, 1);
|
||||||
|
lua_rawget(L, -2); /* 1st result = hooktable[L1] */
|
||||||
|
lua_remove(L, -2); /* remove hook table */
|
||||||
|
}
|
||||||
|
lua_pushstring(L, unmakemask(mask, buff)); /* 2nd result = mask */
|
||||||
|
lua_pushinteger(L, lua_gethookcount(L1)); /* 3rd result = count */
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int db_debug (lua_State *L) {
|
||||||
|
for (;;) {
|
||||||
|
char buffer[250];
|
||||||
|
lua_writestringerror("%s", "lua_debug> ");
|
||||||
|
if (fgets(buffer, sizeof(buffer), stdin) == NULL ||
|
||||||
|
strcmp(buffer, "cont\n") == 0)
|
||||||
|
return 0;
|
||||||
|
if (luaL_loadbuffer(L, buffer, strlen(buffer), "=(debug command)") ||
|
||||||
|
lua_pcall(L, 0, 0, 0))
|
||||||
|
lua_writestringerror("%s\n", luaL_tolstring(L, -1, NULL));
|
||||||
|
lua_settop(L, 0); /* remove eventual returns */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int db_traceback (lua_State *L) {
|
||||||
|
int arg;
|
||||||
|
lua_State *L1 = getthread(L, &arg);
|
||||||
|
const char *msg = lua_tostring(L, arg + 1);
|
||||||
|
if (msg == NULL && !lua_isnoneornil(L, arg + 1)) /* non-string 'msg'? */
|
||||||
|
lua_pushvalue(L, arg + 1); /* return it untouched */
|
||||||
|
else {
|
||||||
|
int level = (int)luaL_optinteger(L, arg + 2, (L == L1) ? 1 : 0);
|
||||||
|
luaL_traceback(L, L1, msg, level);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int db_setcstacklimit (lua_State *L) {
|
||||||
|
int limit = (int)luaL_checkinteger(L, 1);
|
||||||
|
int res = lua_setcstacklimit(L, limit);
|
||||||
|
lua_pushinteger(L, res);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static const luaL_Reg dblib[] = {
|
||||||
|
{"debug", db_debug},
|
||||||
|
{"getuservalue", db_getuservalue},
|
||||||
|
{"gethook", db_gethook},
|
||||||
|
{"getinfo", db_getinfo},
|
||||||
|
{"getlocal", db_getlocal},
|
||||||
|
{"getregistry", db_getregistry},
|
||||||
|
{"getmetatable", db_getmetatable},
|
||||||
|
{"getupvalue", db_getupvalue},
|
||||||
|
{"upvaluejoin", db_upvaluejoin},
|
||||||
|
{"upvalueid", db_upvalueid},
|
||||||
|
{"setuservalue", db_setuservalue},
|
||||||
|
{"sethook", db_sethook},
|
||||||
|
{"setlocal", db_setlocal},
|
||||||
|
{"setmetatable", db_setmetatable},
|
||||||
|
{"setupvalue", db_setupvalue},
|
||||||
|
{"traceback", db_traceback},
|
||||||
|
{"setcstacklimit", db_setcstacklimit},
|
||||||
|
{NULL, NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
LUAMOD_API int luaopen_debug (lua_State *L) {
|
||||||
|
luaL_newlib(L, dblib);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,962 @@
|
|||||||
|
/*
|
||||||
|
** $Id: ldebug.c $
|
||||||
|
** Debug Interface
|
||||||
|
** See Copyright Notice in lua.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define ldebug_c
|
||||||
|
#define LUA_CORE
|
||||||
|
|
||||||
|
#include "lprefix.h"
|
||||||
|
|
||||||
|
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "lua.h"
|
||||||
|
|
||||||
|
#include "lapi.h"
|
||||||
|
#include "lcode.h"
|
||||||
|
#include "ldebug.h"
|
||||||
|
#include "ldo.h"
|
||||||
|
#include "lfunc.h"
|
||||||
|
#include "lobject.h"
|
||||||
|
#include "lopcodes.h"
|
||||||
|
#include "lstate.h"
|
||||||
|
#include "lstring.h"
|
||||||
|
#include "ltable.h"
|
||||||
|
#include "ltm.h"
|
||||||
|
#include "lvm.h"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#define LuaClosure(f) ((f) != NULL && (f)->c.tt == LUA_VLCL)
|
||||||
|
|
||||||
|
|
||||||
|
static const char *funcnamefromcall (lua_State *L, CallInfo *ci,
|
||||||
|
const char **name);
|
||||||
|
|
||||||
|
|
||||||
|
static int currentpc (CallInfo *ci) {
|
||||||
|
lua_assert(isLua(ci));
|
||||||
|
return pcRel(ci->u.l.savedpc, ci_func(ci)->p);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Get a "base line" to find the line corresponding to an instruction.
|
||||||
|
** Base lines are regularly placed at MAXIWTHABS intervals, so usually
|
||||||
|
** an integer division gets the right place. When the source file has
|
||||||
|
** large sequences of empty/comment lines, it may need extra entries,
|
||||||
|
** so the original estimate needs a correction.
|
||||||
|
** If the original estimate is -1, the initial 'if' ensures that the
|
||||||
|
** 'while' will run at least once.
|
||||||
|
** The assertion that the estimate is a lower bound for the correct base
|
||||||
|
** is valid as long as the debug info has been generated with the same
|
||||||
|
** value for MAXIWTHABS or smaller. (Previous releases use a little
|
||||||
|
** smaller value.)
|
||||||
|
*/
|
||||||
|
static int getbaseline (const Proto *f, int pc, int *basepc) {
|
||||||
|
if (f->sizeabslineinfo == 0 || pc < f->abslineinfo[0].pc) {
|
||||||
|
*basepc = -1; /* start from the beginning */
|
||||||
|
return f->linedefined;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
int i = cast_uint(pc) / MAXIWTHABS - 1; /* get an estimate */
|
||||||
|
/* estimate must be a lower bound of the correct base */
|
||||||
|
lua_assert(i < 0 ||
|
||||||
|
(i < f->sizeabslineinfo && f->abslineinfo[i].pc <= pc));
|
||||||
|
while (i + 1 < f->sizeabslineinfo && pc >= f->abslineinfo[i + 1].pc)
|
||||||
|
i++; /* low estimate; adjust it */
|
||||||
|
*basepc = f->abslineinfo[i].pc;
|
||||||
|
return f->abslineinfo[i].line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Get the line corresponding to instruction 'pc' in function 'f';
|
||||||
|
** first gets a base line and from there does the increments until
|
||||||
|
** the desired instruction.
|
||||||
|
*/
|
||||||
|
int luaG_getfuncline (const Proto *f, int pc) {
|
||||||
|
if (f->lineinfo == NULL) /* no debug information? */
|
||||||
|
return -1;
|
||||||
|
else {
|
||||||
|
int basepc;
|
||||||
|
int baseline = getbaseline(f, pc, &basepc);
|
||||||
|
while (basepc++ < pc) { /* walk until given instruction */
|
||||||
|
lua_assert(f->lineinfo[basepc] != ABSLINEINFO);
|
||||||
|
baseline += f->lineinfo[basepc]; /* correct line */
|
||||||
|
}
|
||||||
|
return baseline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int getcurrentline (CallInfo *ci) {
|
||||||
|
return luaG_getfuncline(ci_func(ci)->p, currentpc(ci));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Set 'trap' for all active Lua frames.
|
||||||
|
** This function can be called during a signal, under "reasonable"
|
||||||
|
** assumptions. A new 'ci' is completely linked in the list before it
|
||||||
|
** becomes part of the "active" list, and we assume that pointers are
|
||||||
|
** atomic; see comment in next function.
|
||||||
|
** (A compiler doing interprocedural optimizations could, theoretically,
|
||||||
|
** reorder memory writes in such a way that the list could be
|
||||||
|
** temporarily broken while inserting a new element. We simply assume it
|
||||||
|
** has no good reasons to do that.)
|
||||||
|
*/
|
||||||
|
static void settraps (CallInfo *ci) {
|
||||||
|
for (; ci != NULL; ci = ci->previous)
|
||||||
|
if (isLua(ci))
|
||||||
|
ci->u.l.trap = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** This function can be called during a signal, under "reasonable"
|
||||||
|
** assumptions.
|
||||||
|
** Fields 'basehookcount' and 'hookcount' (set by 'resethookcount')
|
||||||
|
** are for debug only, and it is no problem if they get arbitrary
|
||||||
|
** values (causes at most one wrong hook call). 'hookmask' is an atomic
|
||||||
|
** value. We assume that pointers are atomic too (e.g., gcc ensures that
|
||||||
|
** for all platforms where it runs). Moreover, 'hook' is always checked
|
||||||
|
** before being called (see 'luaD_hook').
|
||||||
|
*/
|
||||||
|
LUA_API void lua_sethook (lua_State *L, lua_Hook func, int mask, int count) {
|
||||||
|
if (func == NULL || mask == 0) { /* turn off hooks? */
|
||||||
|
mask = 0;
|
||||||
|
func = NULL;
|
||||||
|
}
|
||||||
|
L->hook = func;
|
||||||
|
L->basehookcount = count;
|
||||||
|
resethookcount(L);
|
||||||
|
L->hookmask = cast_byte(mask);
|
||||||
|
if (mask)
|
||||||
|
settraps(L->ci); /* to trace inside 'luaV_execute' */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
LUA_API lua_Hook lua_gethook (lua_State *L) {
|
||||||
|
return L->hook;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
LUA_API int lua_gethookmask (lua_State *L) {
|
||||||
|
return L->hookmask;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
LUA_API int lua_gethookcount (lua_State *L) {
|
||||||
|
return L->basehookcount;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
LUA_API int lua_getstack (lua_State *L, int level, lua_Debug *ar) {
|
||||||
|
int status;
|
||||||
|
CallInfo *ci;
|
||||||
|
if (level < 0) return 0; /* invalid (negative) level */
|
||||||
|
lua_lock(L);
|
||||||
|
for (ci = L->ci; level > 0 && ci != &L->base_ci; ci = ci->previous)
|
||||||
|
level--;
|
||||||
|
if (level == 0 && ci != &L->base_ci) { /* level found? */
|
||||||
|
status = 1;
|
||||||
|
ar->i_ci = ci;
|
||||||
|
}
|
||||||
|
else status = 0; /* no such level */
|
||||||
|
lua_unlock(L);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static const char *upvalname (const Proto *p, int uv) {
|
||||||
|
TString *s = check_exp(uv < p->sizeupvalues, p->upvalues[uv].name);
|
||||||
|
if (s == NULL) return "?";
|
||||||
|
else return getstr(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static const char *findvararg (CallInfo *ci, int n, StkId *pos) {
|
||||||
|
if (clLvalue(s2v(ci->func.p))->p->is_vararg) {
|
||||||
|
int nextra = ci->u.l.nextraargs;
|
||||||
|
if (n >= -nextra) { /* 'n' is negative */
|
||||||
|
*pos = ci->func.p - nextra - (n + 1);
|
||||||
|
return "(vararg)"; /* generic name for any vararg */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL; /* no such vararg */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const char *luaG_findlocal (lua_State *L, CallInfo *ci, int n, StkId *pos) {
|
||||||
|
StkId base = ci->func.p + 1;
|
||||||
|
const char *name = NULL;
|
||||||
|
if (isLua(ci)) {
|
||||||
|
if (n < 0) /* access to vararg values? */
|
||||||
|
return findvararg(ci, n, pos);
|
||||||
|
else
|
||||||
|
name = luaF_getlocalname(ci_func(ci)->p, n, currentpc(ci));
|
||||||
|
}
|
||||||
|
if (name == NULL) { /* no 'standard' name? */
|
||||||
|
StkId limit = (ci == L->ci) ? L->top.p : ci->next->func.p;
|
||||||
|
if (limit - base >= n && n > 0) { /* is 'n' inside 'ci' stack? */
|
||||||
|
/* generic name for any valid slot */
|
||||||
|
name = isLua(ci) ? "(temporary)" : "(C temporary)";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return NULL; /* no name */
|
||||||
|
}
|
||||||
|
if (pos)
|
||||||
|
*pos = base + (n - 1);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
LUA_API const char *lua_getlocal (lua_State *L, const lua_Debug *ar, int n) {
|
||||||
|
const char *name;
|
||||||
|
lua_lock(L);
|
||||||
|
if (ar == NULL) { /* information about non-active function? */
|
||||||
|
if (!isLfunction(s2v(L->top.p - 1))) /* not a Lua function? */
|
||||||
|
name = NULL;
|
||||||
|
else /* consider live variables at function start (parameters) */
|
||||||
|
name = luaF_getlocalname(clLvalue(s2v(L->top.p - 1))->p, n, 0);
|
||||||
|
}
|
||||||
|
else { /* active function; get information through 'ar' */
|
||||||
|
StkId pos = NULL; /* to avoid warnings */
|
||||||
|
name = luaG_findlocal(L, ar->i_ci, n, &pos);
|
||||||
|
if (name) {
|
||||||
|
setobjs2s(L, L->top.p, pos);
|
||||||
|
api_incr_top(L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lua_unlock(L);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
LUA_API const char *lua_setlocal (lua_State *L, const lua_Debug *ar, int n) {
|
||||||
|
StkId pos = NULL; /* to avoid warnings */
|
||||||
|
const char *name;
|
||||||
|
lua_lock(L);
|
||||||
|
name = luaG_findlocal(L, ar->i_ci, n, &pos);
|
||||||
|
if (name) {
|
||||||
|
setobjs2s(L, pos, L->top.p - 1);
|
||||||
|
L->top.p--; /* pop value */
|
||||||
|
}
|
||||||
|
lua_unlock(L);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void funcinfo (lua_Debug *ar, Closure *cl) {
|
||||||
|
if (!LuaClosure(cl)) {
|
||||||
|
ar->source = "=[C]";
|
||||||
|
ar->srclen = LL("=[C]");
|
||||||
|
ar->linedefined = -1;
|
||||||
|
ar->lastlinedefined = -1;
|
||||||
|
ar->what = "C";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const Proto *p = cl->l.p;
|
||||||
|
if (p->source) {
|
||||||
|
ar->source = getstr(p->source);
|
||||||
|
ar->srclen = tsslen(p->source);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ar->source = "=?";
|
||||||
|
ar->srclen = LL("=?");
|
||||||
|
}
|
||||||
|
ar->linedefined = p->linedefined;
|
||||||
|
ar->lastlinedefined = p->lastlinedefined;
|
||||||
|
ar->what = (ar->linedefined == 0) ? "main" : "Lua";
|
||||||
|
}
|
||||||
|
luaO_chunkid(ar->short_src, ar->source, ar->srclen);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int nextline (const Proto *p, int currentline, int pc) {
|
||||||
|
if (p->lineinfo[pc] != ABSLINEINFO)
|
||||||
|
return currentline + p->lineinfo[pc];
|
||||||
|
else
|
||||||
|
return luaG_getfuncline(p, pc);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void collectvalidlines (lua_State *L, Closure *f) {
|
||||||
|
if (!LuaClosure(f)) {
|
||||||
|
setnilvalue(s2v(L->top.p));
|
||||||
|
api_incr_top(L);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const Proto *p = f->l.p;
|
||||||
|
int currentline = p->linedefined;
|
||||||
|
Table *t = luaH_new(L); /* new table to store active lines */
|
||||||
|
sethvalue2s(L, L->top.p, t); /* push it on stack */
|
||||||
|
api_incr_top(L);
|
||||||
|
if (p->lineinfo != NULL) { /* proto with debug information? */
|
||||||
|
int i;
|
||||||
|
TValue v;
|
||||||
|
setbtvalue(&v); /* boolean 'true' to be the value of all indices */
|
||||||
|
if (!p->is_vararg) /* regular function? */
|
||||||
|
i = 0; /* consider all instructions */
|
||||||
|
else { /* vararg function */
|
||||||
|
lua_assert(GET_OPCODE(p->code[0]) == OP_VARARGPREP);
|
||||||
|
currentline = nextline(p, currentline, 0);
|
||||||
|
i = 1; /* skip first instruction (OP_VARARGPREP) */
|
||||||
|
}
|
||||||
|
for (; i < p->sizelineinfo; i++) { /* for each instruction */
|
||||||
|
currentline = nextline(p, currentline, i); /* get its line */
|
||||||
|
luaH_setint(L, t, currentline, &v); /* table[line] = true */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static const char *getfuncname (lua_State *L, CallInfo *ci, const char **name) {
|
||||||
|
/* calling function is a known function? */
|
||||||
|
if (ci != NULL && !(ci->callstatus & CIST_TAIL))
|
||||||
|
return funcnamefromcall(L, ci->previous, name);
|
||||||
|
else return NULL; /* no way to find a name */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int auxgetinfo (lua_State *L, const char *what, lua_Debug *ar,
|
||||||
|
Closure *f, CallInfo *ci) {
|
||||||
|
int status = 1;
|
||||||
|
for (; *what; what++) {
|
||||||
|
switch (*what) {
|
||||||
|
case 'S': {
|
||||||
|
funcinfo(ar, f);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'l': {
|
||||||
|
ar->currentline = (ci && isLua(ci)) ? getcurrentline(ci) : -1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'u': {
|
||||||
|
ar->nups = (f == NULL) ? 0 : f->c.nupvalues;
|
||||||
|
if (!LuaClosure(f)) {
|
||||||
|
ar->isvararg = 1;
|
||||||
|
ar->nparams = 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ar->isvararg = f->l.p->is_vararg;
|
||||||
|
ar->nparams = f->l.p->numparams;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 't': {
|
||||||
|
ar->istailcall = (ci) ? ci->callstatus & CIST_TAIL : 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'n': {
|
||||||
|
ar->namewhat = getfuncname(L, ci, &ar->name);
|
||||||
|
if (ar->namewhat == NULL) {
|
||||||
|
ar->namewhat = ""; /* not found */
|
||||||
|
ar->name = NULL;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'r': {
|
||||||
|
if (ci == NULL || !(ci->callstatus & CIST_TRAN))
|
||||||
|
ar->ftransfer = ar->ntransfer = 0;
|
||||||
|
else {
|
||||||
|
ar->ftransfer = ci->u2.transferinfo.ftransfer;
|
||||||
|
ar->ntransfer = ci->u2.transferinfo.ntransfer;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'L':
|
||||||
|
case 'f': /* handled by lua_getinfo */
|
||||||
|
break;
|
||||||
|
default: status = 0; /* invalid option */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
LUA_API int lua_getinfo (lua_State *L, const char *what, lua_Debug *ar) {
|
||||||
|
int status;
|
||||||
|
Closure *cl;
|
||||||
|
CallInfo *ci;
|
||||||
|
TValue *func;
|
||||||
|
lua_lock(L);
|
||||||
|
if (*what == '>') {
|
||||||
|
ci = NULL;
|
||||||
|
func = s2v(L->top.p - 1);
|
||||||
|
api_check(L, ttisfunction(func), "function expected");
|
||||||
|
what++; /* skip the '>' */
|
||||||
|
L->top.p--; /* pop function */
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ci = ar->i_ci;
|
||||||
|
func = s2v(ci->func.p);
|
||||||
|
lua_assert(ttisfunction(func));
|
||||||
|
}
|
||||||
|
cl = ttisclosure(func) ? clvalue(func) : NULL;
|
||||||
|
status = auxgetinfo(L, what, ar, cl, ci);
|
||||||
|
if (strchr(what, 'f')) {
|
||||||
|
setobj2s(L, L->top.p, func);
|
||||||
|
api_incr_top(L);
|
||||||
|
}
|
||||||
|
if (strchr(what, 'L'))
|
||||||
|
collectvalidlines(L, cl);
|
||||||
|
lua_unlock(L);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** {======================================================
|
||||||
|
** Symbolic Execution
|
||||||
|
** =======================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
static int filterpc (int pc, int jmptarget) {
|
||||||
|
if (pc < jmptarget) /* is code conditional (inside a jump)? */
|
||||||
|
return -1; /* cannot know who sets that register */
|
||||||
|
else return pc; /* current position sets that register */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Try to find last instruction before 'lastpc' that modified register 'reg'.
|
||||||
|
*/
|
||||||
|
static int findsetreg (const Proto *p, int lastpc, int reg) {
|
||||||
|
int pc;
|
||||||
|
int setreg = -1; /* keep last instruction that changed 'reg' */
|
||||||
|
int jmptarget = 0; /* any code before this address is conditional */
|
||||||
|
if (testMMMode(GET_OPCODE(p->code[lastpc])))
|
||||||
|
lastpc--; /* previous instruction was not actually executed */
|
||||||
|
for (pc = 0; pc < lastpc; pc++) {
|
||||||
|
Instruction i = p->code[pc];
|
||||||
|
OpCode op = GET_OPCODE(i);
|
||||||
|
int a = GETARG_A(i);
|
||||||
|
int change; /* true if current instruction changed 'reg' */
|
||||||
|
switch (op) {
|
||||||
|
case OP_LOADNIL: { /* set registers from 'a' to 'a+b' */
|
||||||
|
int b = GETARG_B(i);
|
||||||
|
change = (a <= reg && reg <= a + b);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OP_TFORCALL: { /* affect all regs above its base */
|
||||||
|
change = (reg >= a + 2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OP_CALL:
|
||||||
|
case OP_TAILCALL: { /* affect all registers above base */
|
||||||
|
change = (reg >= a);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OP_JMP: { /* doesn't change registers, but changes 'jmptarget' */
|
||||||
|
int b = GETARG_sJ(i);
|
||||||
|
int dest = pc + 1 + b;
|
||||||
|
/* jump does not skip 'lastpc' and is larger than current one? */
|
||||||
|
if (dest <= lastpc && dest > jmptarget)
|
||||||
|
jmptarget = dest; /* update 'jmptarget' */
|
||||||
|
change = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: /* any instruction that sets A */
|
||||||
|
change = (testAMode(op) && reg == a);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (change)
|
||||||
|
setreg = filterpc(pc, jmptarget);
|
||||||
|
}
|
||||||
|
return setreg;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Find a "name" for the constant 'c'.
|
||||||
|
*/
|
||||||
|
static const char *kname (const Proto *p, int index, const char **name) {
|
||||||
|
TValue *kvalue = &p->k[index];
|
||||||
|
if (ttisstring(kvalue)) {
|
||||||
|
*name = getstr(tsvalue(kvalue));
|
||||||
|
return "constant";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
*name = "?";
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static const char *basicgetobjname (const Proto *p, int *ppc, int reg,
|
||||||
|
const char **name) {
|
||||||
|
int pc = *ppc;
|
||||||
|
*name = luaF_getlocalname(p, reg + 1, pc);
|
||||||
|
if (*name) /* is a local? */
|
||||||
|
return "local";
|
||||||
|
/* else try symbolic execution */
|
||||||
|
*ppc = pc = findsetreg(p, pc, reg);
|
||||||
|
if (pc != -1) { /* could find instruction? */
|
||||||
|
Instruction i = p->code[pc];
|
||||||
|
OpCode op = GET_OPCODE(i);
|
||||||
|
switch (op) {
|
||||||
|
case OP_MOVE: {
|
||||||
|
int b = GETARG_B(i); /* move from 'b' to 'a' */
|
||||||
|
if (b < GETARG_A(i))
|
||||||
|
return basicgetobjname(p, ppc, b, name); /* get name for 'b' */
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OP_GETUPVAL: {
|
||||||
|
*name = upvalname(p, GETARG_B(i));
|
||||||
|
return "upvalue";
|
||||||
|
}
|
||||||
|
case OP_LOADK: return kname(p, GETARG_Bx(i), name);
|
||||||
|
case OP_LOADKX: return kname(p, GETARG_Ax(p->code[pc + 1]), name);
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL; /* could not find reasonable name */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Find a "name" for the register 'c'.
|
||||||
|
*/
|
||||||
|
static void rname (const Proto *p, int pc, int c, const char **name) {
|
||||||
|
const char *what = basicgetobjname(p, &pc, c, name); /* search for 'c' */
|
||||||
|
if (!(what && *what == 'c')) /* did not find a constant name? */
|
||||||
|
*name = "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Find a "name" for a 'C' value in an RK instruction.
|
||||||
|
*/
|
||||||
|
static void rkname (const Proto *p, int pc, Instruction i, const char **name) {
|
||||||
|
int c = GETARG_C(i); /* key index */
|
||||||
|
if (GETARG_k(i)) /* is 'c' a constant? */
|
||||||
|
kname(p, c, name);
|
||||||
|
else /* 'c' is a register */
|
||||||
|
rname(p, pc, c, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Check whether table being indexed by instruction 'i' is the
|
||||||
|
** environment '_ENV'
|
||||||
|
*/
|
||||||
|
static const char *isEnv (const Proto *p, int pc, Instruction i, int isup) {
|
||||||
|
int t = GETARG_B(i); /* table index */
|
||||||
|
const char *name; /* name of indexed variable */
|
||||||
|
if (isup) /* is 't' an upvalue? */
|
||||||
|
name = upvalname(p, t);
|
||||||
|
else /* 't' is a register */
|
||||||
|
basicgetobjname(p, &pc, t, &name);
|
||||||
|
return (name && strcmp(name, LUA_ENV) == 0) ? "global" : "field";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Extend 'basicgetobjname' to handle table accesses
|
||||||
|
*/
|
||||||
|
static const char *getobjname (const Proto *p, int lastpc, int reg,
|
||||||
|
const char **name) {
|
||||||
|
const char *kind = basicgetobjname(p, &lastpc, reg, name);
|
||||||
|
if (kind != NULL)
|
||||||
|
return kind;
|
||||||
|
else if (lastpc != -1) { /* could find instruction? */
|
||||||
|
Instruction i = p->code[lastpc];
|
||||||
|
OpCode op = GET_OPCODE(i);
|
||||||
|
switch (op) {
|
||||||
|
case OP_GETTABUP: {
|
||||||
|
int k = GETARG_C(i); /* key index */
|
||||||
|
kname(p, k, name);
|
||||||
|
return isEnv(p, lastpc, i, 1);
|
||||||
|
}
|
||||||
|
case OP_GETTABLE: {
|
||||||
|
int k = GETARG_C(i); /* key index */
|
||||||
|
rname(p, lastpc, k, name);
|
||||||
|
return isEnv(p, lastpc, i, 0);
|
||||||
|
}
|
||||||
|
case OP_GETI: {
|
||||||
|
*name = "integer index";
|
||||||
|
return "field";
|
||||||
|
}
|
||||||
|
case OP_GETFIELD: {
|
||||||
|
int k = GETARG_C(i); /* key index */
|
||||||
|
kname(p, k, name);
|
||||||
|
return isEnv(p, lastpc, i, 0);
|
||||||
|
}
|
||||||
|
case OP_SELF: {
|
||||||
|
rkname(p, lastpc, i, name);
|
||||||
|
return "method";
|
||||||
|
}
|
||||||
|
default: break; /* go through to return NULL */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL; /* could not find reasonable name */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Try to find a name for a function based on the code that called it.
|
||||||
|
** (Only works when function was called by a Lua function.)
|
||||||
|
** Returns what the name is (e.g., "for iterator", "method",
|
||||||
|
** "metamethod") and sets '*name' to point to the name.
|
||||||
|
*/
|
||||||
|
static const char *funcnamefromcode (lua_State *L, const Proto *p,
|
||||||
|
int pc, const char **name) {
|
||||||
|
TMS tm = (TMS)0; /* (initial value avoids warnings) */
|
||||||
|
Instruction i = p->code[pc]; /* calling instruction */
|
||||||
|
switch (GET_OPCODE(i)) {
|
||||||
|
case OP_CALL:
|
||||||
|
case OP_TAILCALL:
|
||||||
|
return getobjname(p, pc, GETARG_A(i), name); /* get function name */
|
||||||
|
case OP_TFORCALL: { /* for iterator */
|
||||||
|
*name = "for iterator";
|
||||||
|
return "for iterator";
|
||||||
|
}
|
||||||
|
/* other instructions can do calls through metamethods */
|
||||||
|
case OP_SELF: case OP_GETTABUP: case OP_GETTABLE:
|
||||||
|
case OP_GETI: case OP_GETFIELD:
|
||||||
|
tm = TM_INDEX;
|
||||||
|
break;
|
||||||
|
case OP_SETTABUP: case OP_SETTABLE: case OP_SETI: case OP_SETFIELD:
|
||||||
|
tm = TM_NEWINDEX;
|
||||||
|
break;
|
||||||
|
case OP_MMBIN: case OP_MMBINI: case OP_MMBINK: {
|
||||||
|
tm = cast(TMS, GETARG_C(i));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OP_UNM: tm = TM_UNM; break;
|
||||||
|
case OP_BNOT: tm = TM_BNOT; break;
|
||||||
|
case OP_LEN: tm = TM_LEN; break;
|
||||||
|
case OP_CONCAT: tm = TM_CONCAT; break;
|
||||||
|
case OP_EQ: tm = TM_EQ; break;
|
||||||
|
/* no cases for OP_EQI and OP_EQK, as they don't call metamethods */
|
||||||
|
case OP_LT: case OP_LTI: case OP_GTI: tm = TM_LT; break;
|
||||||
|
case OP_LE: case OP_LEI: case OP_GEI: tm = TM_LE; break;
|
||||||
|
case OP_CLOSE: case OP_RETURN: tm = TM_CLOSE; break;
|
||||||
|
default:
|
||||||
|
return NULL; /* cannot find a reasonable name */
|
||||||
|
}
|
||||||
|
*name = getshrstr(G(L)->tmname[tm]) + 2;
|
||||||
|
return "metamethod";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Try to find a name for a function based on how it was called.
|
||||||
|
*/
|
||||||
|
static const char *funcnamefromcall (lua_State *L, CallInfo *ci,
|
||||||
|
const char **name) {
|
||||||
|
if (ci->callstatus & CIST_HOOKED) { /* was it called inside a hook? */
|
||||||
|
*name = "?";
|
||||||
|
return "hook";
|
||||||
|
}
|
||||||
|
else if (ci->callstatus & CIST_FIN) { /* was it called as a finalizer? */
|
||||||
|
*name = "__gc";
|
||||||
|
return "metamethod"; /* report it as such */
|
||||||
|
}
|
||||||
|
else if (isLua(ci))
|
||||||
|
return funcnamefromcode(L, ci_func(ci)->p, currentpc(ci), name);
|
||||||
|
else
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* }====================================================== */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Check whether pointer 'o' points to some value in the stack frame of
|
||||||
|
** the current function and, if so, returns its index. Because 'o' may
|
||||||
|
** not point to a value in this stack, we cannot compare it with the
|
||||||
|
** region boundaries (undefined behavior in ISO C).
|
||||||
|
*/
|
||||||
|
static int instack (CallInfo *ci, const TValue *o) {
|
||||||
|
int pos;
|
||||||
|
StkId base = ci->func.p + 1;
|
||||||
|
for (pos = 0; base + pos < ci->top.p; pos++) {
|
||||||
|
if (o == s2v(base + pos))
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
return -1; /* not found */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Checks whether value 'o' came from an upvalue. (That can only happen
|
||||||
|
** with instructions OP_GETTABUP/OP_SETTABUP, which operate directly on
|
||||||
|
** upvalues.)
|
||||||
|
*/
|
||||||
|
static const char *getupvalname (CallInfo *ci, const TValue *o,
|
||||||
|
const char **name) {
|
||||||
|
LClosure *c = ci_func(ci);
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < c->nupvalues; i++) {
|
||||||
|
if (c->upvals[i]->v.p == o) {
|
||||||
|
*name = upvalname(c->p, i);
|
||||||
|
return "upvalue";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static const char *formatvarinfo (lua_State *L, const char *kind,
|
||||||
|
const char *name) {
|
||||||
|
if (kind == NULL)
|
||||||
|
return ""; /* no information */
|
||||||
|
else
|
||||||
|
return luaO_pushfstring(L, " (%s '%s')", kind, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Build a string with a "description" for the value 'o', such as
|
||||||
|
** "variable 'x'" or "upvalue 'y'".
|
||||||
|
*/
|
||||||
|
static const char *varinfo (lua_State *L, const TValue *o) {
|
||||||
|
CallInfo *ci = L->ci;
|
||||||
|
const char *name = NULL; /* to avoid warnings */
|
||||||
|
const char *kind = NULL;
|
||||||
|
if (isLua(ci)) {
|
||||||
|
kind = getupvalname(ci, o, &name); /* check whether 'o' is an upvalue */
|
||||||
|
if (!kind) { /* not an upvalue? */
|
||||||
|
int reg = instack(ci, o); /* try a register */
|
||||||
|
if (reg >= 0) /* is 'o' a register? */
|
||||||
|
kind = getobjname(ci_func(ci)->p, currentpc(ci), reg, &name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return formatvarinfo(L, kind, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Raise a type error
|
||||||
|
*/
|
||||||
|
static l_noret typeerror (lua_State *L, const TValue *o, const char *op,
|
||||||
|
const char *extra) {
|
||||||
|
const char *t = luaT_objtypename(L, o);
|
||||||
|
luaG_runerror(L, "attempt to %s a %s value%s", op, t, extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Raise a type error with "standard" information about the faulty
|
||||||
|
** object 'o' (using 'varinfo').
|
||||||
|
*/
|
||||||
|
l_noret luaG_typeerror (lua_State *L, const TValue *o, const char *op) {
|
||||||
|
typeerror(L, o, op, varinfo(L, o));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Raise an error for calling a non-callable object. Try to find a name
|
||||||
|
** for the object based on how it was called ('funcnamefromcall'); if it
|
||||||
|
** cannot get a name there, try 'varinfo'.
|
||||||
|
*/
|
||||||
|
l_noret luaG_callerror (lua_State *L, const TValue *o) {
|
||||||
|
CallInfo *ci = L->ci;
|
||||||
|
const char *name = NULL; /* to avoid warnings */
|
||||||
|
const char *kind = funcnamefromcall(L, ci, &name);
|
||||||
|
const char *extra = kind ? formatvarinfo(L, kind, name) : varinfo(L, o);
|
||||||
|
typeerror(L, o, "call", extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
l_noret luaG_forerror (lua_State *L, const TValue *o, const char *what) {
|
||||||
|
luaG_runerror(L, "bad 'for' %s (number expected, got %s)",
|
||||||
|
what, luaT_objtypename(L, o));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
l_noret luaG_concaterror (lua_State *L, const TValue *p1, const TValue *p2) {
|
||||||
|
if (ttisstring(p1) || cvt2str(p1)) p1 = p2;
|
||||||
|
luaG_typeerror(L, p1, "concatenate");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
l_noret luaG_opinterror (lua_State *L, const TValue *p1,
|
||||||
|
const TValue *p2, const char *msg) {
|
||||||
|
if (!ttisnumber(p1)) /* first operand is wrong? */
|
||||||
|
p2 = p1; /* now second is wrong */
|
||||||
|
luaG_typeerror(L, p2, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Error when both values are convertible to numbers, but not to integers
|
||||||
|
*/
|
||||||
|
l_noret luaG_tointerror (lua_State *L, const TValue *p1, const TValue *p2) {
|
||||||
|
lua_Integer temp;
|
||||||
|
if (!luaV_tointegerns(p1, &temp, LUA_FLOORN2I))
|
||||||
|
p2 = p1;
|
||||||
|
luaG_runerror(L, "number%s has no integer representation", varinfo(L, p2));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
l_noret luaG_ordererror (lua_State *L, const TValue *p1, const TValue *p2) {
|
||||||
|
const char *t1 = luaT_objtypename(L, p1);
|
||||||
|
const char *t2 = luaT_objtypename(L, p2);
|
||||||
|
if (strcmp(t1, t2) == 0)
|
||||||
|
luaG_runerror(L, "attempt to compare two %s values", t1);
|
||||||
|
else
|
||||||
|
luaG_runerror(L, "attempt to compare %s with %s", t1, t2);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* add src:line information to 'msg' */
|
||||||
|
const char *luaG_addinfo (lua_State *L, const char *msg, TString *src,
|
||||||
|
int line) {
|
||||||
|
char buff[LUA_IDSIZE];
|
||||||
|
if (src)
|
||||||
|
luaO_chunkid(buff, getstr(src), tsslen(src));
|
||||||
|
else { /* no source available; use "?" instead */
|
||||||
|
buff[0] = '?'; buff[1] = '\0';
|
||||||
|
}
|
||||||
|
return luaO_pushfstring(L, "%s:%d: %s", buff, line, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
l_noret luaG_errormsg (lua_State *L) {
|
||||||
|
if (L->errfunc != 0) { /* is there an error handling function? */
|
||||||
|
StkId errfunc = restorestack(L, L->errfunc);
|
||||||
|
lua_assert(ttisfunction(s2v(errfunc)));
|
||||||
|
setobjs2s(L, L->top.p, L->top.p - 1); /* move argument */
|
||||||
|
setobjs2s(L, L->top.p - 1, errfunc); /* push function */
|
||||||
|
L->top.p++; /* assume EXTRA_STACK */
|
||||||
|
luaD_callnoyield(L, L->top.p - 2, 1); /* call it */
|
||||||
|
}
|
||||||
|
luaD_throw(L, LUA_ERRRUN);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
l_noret luaG_runerror (lua_State *L, const char *fmt, ...) {
|
||||||
|
CallInfo *ci = L->ci;
|
||||||
|
const char *msg;
|
||||||
|
va_list argp;
|
||||||
|
luaC_checkGC(L); /* error message uses memory */
|
||||||
|
va_start(argp, fmt);
|
||||||
|
msg = luaO_pushvfstring(L, fmt, argp); /* format message */
|
||||||
|
va_end(argp);
|
||||||
|
if (isLua(ci)) { /* if Lua function, add source:line information */
|
||||||
|
luaG_addinfo(L, msg, ci_func(ci)->p->source, getcurrentline(ci));
|
||||||
|
setobjs2s(L, L->top.p - 2, L->top.p - 1); /* remove 'msg' */
|
||||||
|
L->top.p--;
|
||||||
|
}
|
||||||
|
luaG_errormsg(L);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Check whether new instruction 'newpc' is in a different line from
|
||||||
|
** previous instruction 'oldpc'. More often than not, 'newpc' is only
|
||||||
|
** one or a few instructions after 'oldpc' (it must be after, see
|
||||||
|
** caller), so try to avoid calling 'luaG_getfuncline'. If they are
|
||||||
|
** too far apart, there is a good chance of a ABSLINEINFO in the way,
|
||||||
|
** so it goes directly to 'luaG_getfuncline'.
|
||||||
|
*/
|
||||||
|
static int changedline (const Proto *p, int oldpc, int newpc) {
|
||||||
|
if (p->lineinfo == NULL) /* no debug information? */
|
||||||
|
return 0;
|
||||||
|
if (newpc - oldpc < MAXIWTHABS / 2) { /* not too far apart? */
|
||||||
|
int delta = 0; /* line difference */
|
||||||
|
int pc = oldpc;
|
||||||
|
for (;;) {
|
||||||
|
int lineinfo = p->lineinfo[++pc];
|
||||||
|
if (lineinfo == ABSLINEINFO)
|
||||||
|
break; /* cannot compute delta; fall through */
|
||||||
|
delta += lineinfo;
|
||||||
|
if (pc == newpc)
|
||||||
|
return (delta != 0); /* delta computed successfully */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* either instructions are too far apart or there is an absolute line
|
||||||
|
info in the way; compute line difference explicitly */
|
||||||
|
return (luaG_getfuncline(p, oldpc) != luaG_getfuncline(p, newpc));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Traces Lua calls. If code is running the first instruction of a function,
|
||||||
|
** and function is not vararg, and it is not coming from an yield,
|
||||||
|
** calls 'luaD_hookcall'. (Vararg functions will call 'luaD_hookcall'
|
||||||
|
** after adjusting its variable arguments; otherwise, they could call
|
||||||
|
** a line/count hook before the call hook. Functions coming from
|
||||||
|
** an yield already called 'luaD_hookcall' before yielding.)
|
||||||
|
*/
|
||||||
|
int luaG_tracecall (lua_State *L) {
|
||||||
|
CallInfo *ci = L->ci;
|
||||||
|
Proto *p = ci_func(ci)->p;
|
||||||
|
ci->u.l.trap = 1; /* ensure hooks will be checked */
|
||||||
|
if (ci->u.l.savedpc == p->code) { /* first instruction (not resuming)? */
|
||||||
|
if (p->is_vararg)
|
||||||
|
return 0; /* hooks will start at VARARGPREP instruction */
|
||||||
|
else if (!(ci->callstatus & CIST_HOOKYIELD)) /* not yieded? */
|
||||||
|
luaD_hookcall(L, ci); /* check 'call' hook */
|
||||||
|
}
|
||||||
|
return 1; /* keep 'trap' on */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Traces the execution of a Lua function. Called before the execution
|
||||||
|
** of each opcode, when debug is on. 'L->oldpc' stores the last
|
||||||
|
** instruction traced, to detect line changes. When entering a new
|
||||||
|
** function, 'npci' will be zero and will test as a new line whatever
|
||||||
|
** the value of 'oldpc'. Some exceptional conditions may return to
|
||||||
|
** a function without setting 'oldpc'. In that case, 'oldpc' may be
|
||||||
|
** invalid; if so, use zero as a valid value. (A wrong but valid 'oldpc'
|
||||||
|
** at most causes an extra call to a line hook.)
|
||||||
|
** This function is not "Protected" when called, so it should correct
|
||||||
|
** 'L->top.p' before calling anything that can run the GC.
|
||||||
|
*/
|
||||||
|
int luaG_traceexec (lua_State *L, const Instruction *pc) {
|
||||||
|
CallInfo *ci = L->ci;
|
||||||
|
lu_byte mask = L->hookmask;
|
||||||
|
const Proto *p = ci_func(ci)->p;
|
||||||
|
int counthook;
|
||||||
|
if (!(mask & (LUA_MASKLINE | LUA_MASKCOUNT))) { /* no hooks? */
|
||||||
|
ci->u.l.trap = 0; /* don't need to stop again */
|
||||||
|
return 0; /* turn off 'trap' */
|
||||||
|
}
|
||||||
|
pc++; /* reference is always next instruction */
|
||||||
|
ci->u.l.savedpc = pc; /* save 'pc' */
|
||||||
|
counthook = (mask & LUA_MASKCOUNT) && (--L->hookcount == 0);
|
||||||
|
if (counthook)
|
||||||
|
resethookcount(L); /* reset count */
|
||||||
|
else if (!(mask & LUA_MASKLINE))
|
||||||
|
return 1; /* no line hook and count != 0; nothing to be done now */
|
||||||
|
if (ci->callstatus & CIST_HOOKYIELD) { /* hook yielded last time? */
|
||||||
|
ci->callstatus &= ~CIST_HOOKYIELD; /* erase mark */
|
||||||
|
return 1; /* do not call hook again (VM yielded, so it did not move) */
|
||||||
|
}
|
||||||
|
if (!isIT(*(ci->u.l.savedpc - 1))) /* top not being used? */
|
||||||
|
L->top.p = ci->top.p; /* correct top */
|
||||||
|
if (counthook)
|
||||||
|
luaD_hook(L, LUA_HOOKCOUNT, -1, 0, 0); /* call count hook */
|
||||||
|
if (mask & LUA_MASKLINE) {
|
||||||
|
/* 'L->oldpc' may be invalid; use zero in this case */
|
||||||
|
int oldpc = (L->oldpc < p->sizecode) ? L->oldpc : 0;
|
||||||
|
int npci = pcRel(pc, p);
|
||||||
|
if (npci <= oldpc || /* call hook when jump back (loop), */
|
||||||
|
changedline(p, oldpc, npci)) { /* or when enter new line */
|
||||||
|
int newline = luaG_getfuncline(p, npci);
|
||||||
|
luaD_hook(L, LUA_HOOKLINE, newline, 0, 0); /* call line hook */
|
||||||
|
}
|
||||||
|
L->oldpc = npci; /* 'pc' of last call to line hook */
|
||||||
|
}
|
||||||
|
if (L->status == LUA_YIELD) { /* did hook yield? */
|
||||||
|
if (counthook)
|
||||||
|
L->hookcount = 1; /* undo decrement to zero */
|
||||||
|
ci->callstatus |= CIST_HOOKYIELD; /* mark that it yielded */
|
||||||
|
luaD_throw(L, LUA_YIELD);
|
||||||
|
}
|
||||||
|
return 1; /* keep 'trap' on */
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
** $Id: ldebug.h $
|
||||||
|
** Auxiliary functions from Debug Interface module
|
||||||
|
** See Copyright Notice in lua.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef ldebug_h
|
||||||
|
#define ldebug_h
|
||||||
|
|
||||||
|
|
||||||
|
#include "lstate.h"
|
||||||
|
|
||||||
|
|
||||||
|
#define pcRel(pc, p) (cast_int((pc) - (p)->code) - 1)
|
||||||
|
|
||||||
|
|
||||||
|
/* Active Lua function (given call info) */
|
||||||
|
#define ci_func(ci) (clLvalue(s2v((ci)->func.p)))
|
||||||
|
|
||||||
|
|
||||||
|
#define resethookcount(L) (L->hookcount = L->basehookcount)
|
||||||
|
|
||||||
|
/*
|
||||||
|
** mark for entries in 'lineinfo' array that has absolute information in
|
||||||
|
** 'abslineinfo' array
|
||||||
|
*/
|
||||||
|
#define ABSLINEINFO (-0x80)
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** MAXimum number of successive Instructions WiTHout ABSolute line
|
||||||
|
** information. (A power of two allows fast divisions.)
|
||||||
|
*/
|
||||||
|
#if !defined(MAXIWTHABS)
|
||||||
|
#define MAXIWTHABS 128
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
LUAI_FUNC int luaG_getfuncline (const Proto *f, int pc);
|
||||||
|
LUAI_FUNC const char *luaG_findlocal (lua_State *L, CallInfo *ci, int n,
|
||||||
|
StkId *pos);
|
||||||
|
LUAI_FUNC l_noret luaG_typeerror (lua_State *L, const TValue *o,
|
||||||
|
const char *opname);
|
||||||
|
LUAI_FUNC l_noret luaG_callerror (lua_State *L, const TValue *o);
|
||||||
|
LUAI_FUNC l_noret luaG_forerror (lua_State *L, const TValue *o,
|
||||||
|
const char *what);
|
||||||
|
LUAI_FUNC l_noret luaG_concaterror (lua_State *L, const TValue *p1,
|
||||||
|
const TValue *p2);
|
||||||
|
LUAI_FUNC l_noret luaG_opinterror (lua_State *L, const TValue *p1,
|
||||||
|
const TValue *p2,
|
||||||
|
const char *msg);
|
||||||
|
LUAI_FUNC l_noret luaG_tointerror (lua_State *L, const TValue *p1,
|
||||||
|
const TValue *p2);
|
||||||
|
LUAI_FUNC l_noret luaG_ordererror (lua_State *L, const TValue *p1,
|
||||||
|
const TValue *p2);
|
||||||
|
LUAI_FUNC l_noret luaG_runerror (lua_State *L, const char *fmt, ...);
|
||||||
|
LUAI_FUNC const char *luaG_addinfo (lua_State *L, const char *msg,
|
||||||
|
TString *src, int line);
|
||||||
|
LUAI_FUNC l_noret luaG_errormsg (lua_State *L);
|
||||||
|
LUAI_FUNC int luaG_traceexec (lua_State *L, const Instruction *pc);
|
||||||
|
LUAI_FUNC int luaG_tracecall (lua_State *L);
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
** $Id: ldo.h $
|
||||||
|
** Stack and Call structure of Lua
|
||||||
|
** See Copyright Notice in lua.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef ldo_h
|
||||||
|
#define ldo_h
|
||||||
|
|
||||||
|
|
||||||
|
#include "llimits.h"
|
||||||
|
#include "lobject.h"
|
||||||
|
#include "lstate.h"
|
||||||
|
#include "lzio.h"
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Macro to check stack size and grow stack if needed. Parameters
|
||||||
|
** 'pre'/'pos' allow the macro to preserve a pointer into the
|
||||||
|
** stack across reallocations, doing the work only when needed.
|
||||||
|
** It also allows the running of one GC step when the stack is
|
||||||
|
** reallocated.
|
||||||
|
** 'condmovestack' is used in heavy tests to force a stack reallocation
|
||||||
|
** at every check.
|
||||||
|
*/
|
||||||
|
#define luaD_checkstackaux(L,n,pre,pos) \
|
||||||
|
if (l_unlikely(L->stack_last.p - L->top.p <= (n))) \
|
||||||
|
{ pre; luaD_growstack(L, n, 1); pos; } \
|
||||||
|
else { condmovestack(L,pre,pos); }
|
||||||
|
|
||||||
|
/* In general, 'pre'/'pos' are empty (nothing to save) */
|
||||||
|
#define luaD_checkstack(L,n) luaD_checkstackaux(L,n,(void)0,(void)0)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#define savestack(L,pt) (cast_charp(pt) - cast_charp(L->stack.p))
|
||||||
|
#define restorestack(L,n) cast(StkId, cast_charp(L->stack.p) + (n))
|
||||||
|
|
||||||
|
|
||||||
|
/* macro to check stack size, preserving 'p' */
|
||||||
|
#define checkstackp(L,n,p) \
|
||||||
|
luaD_checkstackaux(L, n, \
|
||||||
|
ptrdiff_t t__ = savestack(L, p), /* save 'p' */ \
|
||||||
|
p = restorestack(L, t__)) /* 'pos' part: restore 'p' */
|
||||||
|
|
||||||
|
|
||||||
|
/* macro to check stack size and GC, preserving 'p' */
|
||||||
|
#define checkstackGCp(L,n,p) \
|
||||||
|
luaD_checkstackaux(L, n, \
|
||||||
|
ptrdiff_t t__ = savestack(L, p); /* save 'p' */ \
|
||||||
|
luaC_checkGC(L), /* stack grow uses memory */ \
|
||||||
|
p = restorestack(L, t__)) /* 'pos' part: restore 'p' */
|
||||||
|
|
||||||
|
|
||||||
|
/* macro to check stack size and GC */
|
||||||
|
#define checkstackGC(L,fsize) \
|
||||||
|
luaD_checkstackaux(L, (fsize), luaC_checkGC(L), (void)0)
|
||||||
|
|
||||||
|
|
||||||
|
/* type of protected functions, to be ran by 'runprotected' */
|
||||||
|
typedef void (*Pfunc) (lua_State *L, void *ud);
|
||||||
|
|
||||||
|
LUAI_FUNC void luaD_seterrorobj (lua_State *L, int errcode, StkId oldtop);
|
||||||
|
LUAI_FUNC int luaD_protectedparser (lua_State *L, ZIO *z, const char *name,
|
||||||
|
const char *mode);
|
||||||
|
LUAI_FUNC void luaD_hook (lua_State *L, int event, int line,
|
||||||
|
int fTransfer, int nTransfer);
|
||||||
|
LUAI_FUNC void luaD_hookcall (lua_State *L, CallInfo *ci);
|
||||||
|
LUAI_FUNC int luaD_pretailcall (lua_State *L, CallInfo *ci, StkId func,
|
||||||
|
int narg1, int delta);
|
||||||
|
LUAI_FUNC CallInfo *luaD_precall (lua_State *L, StkId func, int nResults);
|
||||||
|
LUAI_FUNC void luaD_call (lua_State *L, StkId func, int nResults);
|
||||||
|
LUAI_FUNC void luaD_callnoyield (lua_State *L, StkId func, int nResults);
|
||||||
|
LUAI_FUNC int luaD_closeprotected (lua_State *L, ptrdiff_t level, int status);
|
||||||
|
LUAI_FUNC int luaD_pcall (lua_State *L, Pfunc func, void *u,
|
||||||
|
ptrdiff_t oldtop, ptrdiff_t ef);
|
||||||
|
LUAI_FUNC void luaD_poscall (lua_State *L, CallInfo *ci, int nres);
|
||||||
|
LUAI_FUNC int luaD_reallocstack (lua_State *L, int newsize, int raiseerror);
|
||||||
|
LUAI_FUNC int luaD_growstack (lua_State *L, int n, int raiseerror);
|
||||||
|
LUAI_FUNC void luaD_shrinkstack (lua_State *L);
|
||||||
|
LUAI_FUNC void luaD_inctop (lua_State *L);
|
||||||
|
|
||||||
|
LUAI_FUNC l_noret luaD_throw (lua_State *L, int errcode);
|
||||||
|
LUAI_FUNC int luaD_rawrunprotected (lua_State *L, Pfunc f, void *ud);
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
/*
|
||||||
|
** $Id: ldump.c $
|
||||||
|
** save precompiled Lua chunks
|
||||||
|
** See Copyright Notice in lua.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define ldump_c
|
||||||
|
#define LUA_CORE
|
||||||
|
|
||||||
|
#include "lprefix.h"
|
||||||
|
|
||||||
|
|
||||||
|
#include <limits.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#include "lua.h"
|
||||||
|
|
||||||
|
#include "lobject.h"
|
||||||
|
#include "lstate.h"
|
||||||
|
#include "lundump.h"
|
||||||
|
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
lua_State *L;
|
||||||
|
lua_Writer writer;
|
||||||
|
void *data;
|
||||||
|
int strip;
|
||||||
|
int status;
|
||||||
|
} DumpState;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** All high-level dumps go through dumpVector; you can change it to
|
||||||
|
** change the endianness of the result
|
||||||
|
*/
|
||||||
|
#define dumpVector(D,v,n) dumpBlock(D,v,(n)*sizeof((v)[0]))
|
||||||
|
|
||||||
|
#define dumpLiteral(D, s) dumpBlock(D,s,sizeof(s) - sizeof(char))
|
||||||
|
|
||||||
|
|
||||||
|
static void dumpBlock (DumpState *D, const void *b, size_t size) {
|
||||||
|
if (D->status == 0 && size > 0) {
|
||||||
|
lua_unlock(D->L);
|
||||||
|
D->status = (*D->writer)(D->L, b, size, D->data);
|
||||||
|
lua_lock(D->L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#define dumpVar(D,x) dumpVector(D,&x,1)
|
||||||
|
|
||||||
|
|
||||||
|
static void dumpByte (DumpState *D, int y) {
|
||||||
|
lu_byte x = (lu_byte)y;
|
||||||
|
dumpVar(D, x);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** 'dumpSize' buffer size: each byte can store up to 7 bits. (The "+6"
|
||||||
|
** rounds up the division.)
|
||||||
|
*/
|
||||||
|
#define DIBS ((sizeof(size_t) * CHAR_BIT + 6) / 7)
|
||||||
|
|
||||||
|
static void dumpSize (DumpState *D, size_t x) {
|
||||||
|
lu_byte buff[DIBS];
|
||||||
|
int n = 0;
|
||||||
|
do {
|
||||||
|
buff[DIBS - (++n)] = x & 0x7f; /* fill buffer in reverse order */
|
||||||
|
x >>= 7;
|
||||||
|
} while (x != 0);
|
||||||
|
buff[DIBS - 1] |= 0x80; /* mark last byte */
|
||||||
|
dumpVector(D, buff + DIBS - n, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void dumpInt (DumpState *D, int x) {
|
||||||
|
dumpSize(D, x);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void dumpNumber (DumpState *D, lua_Number x) {
|
||||||
|
dumpVar(D, x);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void dumpInteger (DumpState *D, lua_Integer x) {
|
||||||
|
dumpVar(D, x);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void dumpString (DumpState *D, const TString *s) {
|
||||||
|
if (s == NULL)
|
||||||
|
dumpSize(D, 0);
|
||||||
|
else {
|
||||||
|
size_t size = tsslen(s);
|
||||||
|
const char *str = getstr(s);
|
||||||
|
dumpSize(D, size + 1);
|
||||||
|
dumpVector(D, str, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void dumpCode (DumpState *D, const Proto *f) {
|
||||||
|
dumpInt(D, f->sizecode);
|
||||||
|
dumpVector(D, f->code, f->sizecode);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void dumpFunction(DumpState *D, const Proto *f, TString *psource);
|
||||||
|
|
||||||
|
static void dumpConstants (DumpState *D, const Proto *f) {
|
||||||
|
int i;
|
||||||
|
int n = f->sizek;
|
||||||
|
dumpInt(D, n);
|
||||||
|
for (i = 0; i < n; i++) {
|
||||||
|
const TValue *o = &f->k[i];
|
||||||
|
int tt = ttypetag(o);
|
||||||
|
dumpByte(D, tt);
|
||||||
|
switch (tt) {
|
||||||
|
case LUA_VNUMFLT:
|
||||||
|
dumpNumber(D, fltvalue(o));
|
||||||
|
break;
|
||||||
|
case LUA_VNUMINT:
|
||||||
|
dumpInteger(D, ivalue(o));
|
||||||
|
break;
|
||||||
|
case LUA_VSHRSTR:
|
||||||
|
case LUA_VLNGSTR:
|
||||||
|
dumpString(D, tsvalue(o));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
lua_assert(tt == LUA_VNIL || tt == LUA_VFALSE || tt == LUA_VTRUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void dumpProtos (DumpState *D, const Proto *f) {
|
||||||
|
int i;
|
||||||
|
int n = f->sizep;
|
||||||
|
dumpInt(D, n);
|
||||||
|
for (i = 0; i < n; i++)
|
||||||
|
dumpFunction(D, f->p[i], f->source);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void dumpUpvalues (DumpState *D, const Proto *f) {
|
||||||
|
int i, n = f->sizeupvalues;
|
||||||
|
dumpInt(D, n);
|
||||||
|
for (i = 0; i < n; i++) {
|
||||||
|
dumpByte(D, f->upvalues[i].instack);
|
||||||
|
dumpByte(D, f->upvalues[i].idx);
|
||||||
|
dumpByte(D, f->upvalues[i].kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void dumpDebug (DumpState *D, const Proto *f) {
|
||||||
|
int i, n;
|
||||||
|
n = (D->strip) ? 0 : f->sizelineinfo;
|
||||||
|
dumpInt(D, n);
|
||||||
|
dumpVector(D, f->lineinfo, n);
|
||||||
|
n = (D->strip) ? 0 : f->sizeabslineinfo;
|
||||||
|
dumpInt(D, n);
|
||||||
|
for (i = 0; i < n; i++) {
|
||||||
|
dumpInt(D, f->abslineinfo[i].pc);
|
||||||
|
dumpInt(D, f->abslineinfo[i].line);
|
||||||
|
}
|
||||||
|
n = (D->strip) ? 0 : f->sizelocvars;
|
||||||
|
dumpInt(D, n);
|
||||||
|
for (i = 0; i < n; i++) {
|
||||||
|
dumpString(D, f->locvars[i].varname);
|
||||||
|
dumpInt(D, f->locvars[i].startpc);
|
||||||
|
dumpInt(D, f->locvars[i].endpc);
|
||||||
|
}
|
||||||
|
n = (D->strip) ? 0 : f->sizeupvalues;
|
||||||
|
dumpInt(D, n);
|
||||||
|
for (i = 0; i < n; i++)
|
||||||
|
dumpString(D, f->upvalues[i].name);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void dumpFunction (DumpState *D, const Proto *f, TString *psource) {
|
||||||
|
if (D->strip || f->source == psource)
|
||||||
|
dumpString(D, NULL); /* no debug info or same source as its parent */
|
||||||
|
else
|
||||||
|
dumpString(D, f->source);
|
||||||
|
dumpInt(D, f->linedefined);
|
||||||
|
dumpInt(D, f->lastlinedefined);
|
||||||
|
dumpByte(D, f->numparams);
|
||||||
|
dumpByte(D, f->is_vararg);
|
||||||
|
dumpByte(D, f->maxstacksize);
|
||||||
|
dumpCode(D, f);
|
||||||
|
dumpConstants(D, f);
|
||||||
|
dumpUpvalues(D, f);
|
||||||
|
dumpProtos(D, f);
|
||||||
|
dumpDebug(D, f);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void dumpHeader (DumpState *D) {
|
||||||
|
dumpLiteral(D, LUA_SIGNATURE);
|
||||||
|
dumpByte(D, LUAC_VERSION);
|
||||||
|
dumpByte(D, LUAC_FORMAT);
|
||||||
|
dumpLiteral(D, LUAC_DATA);
|
||||||
|
dumpByte(D, sizeof(Instruction));
|
||||||
|
dumpByte(D, sizeof(lua_Integer));
|
||||||
|
dumpByte(D, sizeof(lua_Number));
|
||||||
|
dumpInteger(D, LUAC_INT);
|
||||||
|
dumpNumber(D, LUAC_NUM);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** dump Lua function as precompiled chunk
|
||||||
|
*/
|
||||||
|
int luaU_dump(lua_State *L, const Proto *f, lua_Writer w, void *data,
|
||||||
|
int strip) {
|
||||||
|
DumpState D;
|
||||||
|
D.L = L;
|
||||||
|
D.writer = w;
|
||||||
|
D.data = data;
|
||||||
|
D.strip = strip;
|
||||||
|
D.status = 0;
|
||||||
|
dumpHeader(&D);
|
||||||
|
dumpByte(&D, f->sizeupvalues);
|
||||||
|
dumpFunction(&D, f, NULL);
|
||||||
|
return D.status;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
/*
|
||||||
|
** $Id: lfunc.c $
|
||||||
|
** Auxiliary functions to manipulate prototypes and closures
|
||||||
|
** See Copyright Notice in lua.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define lfunc_c
|
||||||
|
#define LUA_CORE
|
||||||
|
|
||||||
|
#include "lprefix.h"
|
||||||
|
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#include "lua.h"
|
||||||
|
|
||||||
|
#include "ldebug.h"
|
||||||
|
#include "ldo.h"
|
||||||
|
#include "lfunc.h"
|
||||||
|
#include "lgc.h"
|
||||||
|
#include "lmem.h"
|
||||||
|
#include "lobject.h"
|
||||||
|
#include "lstate.h"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CClosure *luaF_newCclosure (lua_State *L, int nupvals) {
|
||||||
|
GCObject *o = luaC_newobj(L, LUA_VCCL, sizeCclosure(nupvals));
|
||||||
|
CClosure *c = gco2ccl(o);
|
||||||
|
c->nupvalues = cast_byte(nupvals);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
LClosure *luaF_newLclosure (lua_State *L, int nupvals) {
|
||||||
|
GCObject *o = luaC_newobj(L, LUA_VLCL, sizeLclosure(nupvals));
|
||||||
|
LClosure *c = gco2lcl(o);
|
||||||
|
c->p = NULL;
|
||||||
|
c->nupvalues = cast_byte(nupvals);
|
||||||
|
while (nupvals--) c->upvals[nupvals] = NULL;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** fill a closure with new closed upvalues
|
||||||
|
*/
|
||||||
|
void luaF_initupvals (lua_State *L, LClosure *cl) {
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < cl->nupvalues; i++) {
|
||||||
|
GCObject *o = luaC_newobj(L, LUA_VUPVAL, sizeof(UpVal));
|
||||||
|
UpVal *uv = gco2upv(o);
|
||||||
|
uv->v.p = &uv->u.value; /* make it closed */
|
||||||
|
setnilvalue(uv->v.p);
|
||||||
|
cl->upvals[i] = uv;
|
||||||
|
luaC_objbarrier(L, cl, uv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Create a new upvalue at the given level, and link it to the list of
|
||||||
|
** open upvalues of 'L' after entry 'prev'.
|
||||||
|
**/
|
||||||
|
static UpVal *newupval (lua_State *L, StkId level, UpVal **prev) {
|
||||||
|
GCObject *o = luaC_newobj(L, LUA_VUPVAL, sizeof(UpVal));
|
||||||
|
UpVal *uv = gco2upv(o);
|
||||||
|
UpVal *next = *prev;
|
||||||
|
uv->v.p = s2v(level); /* current value lives in the stack */
|
||||||
|
uv->u.open.next = next; /* link it to list of open upvalues */
|
||||||
|
uv->u.open.previous = prev;
|
||||||
|
if (next)
|
||||||
|
next->u.open.previous = &uv->u.open.next;
|
||||||
|
*prev = uv;
|
||||||
|
if (!isintwups(L)) { /* thread not in list of threads with upvalues? */
|
||||||
|
L->twups = G(L)->twups; /* link it to the list */
|
||||||
|
G(L)->twups = L;
|
||||||
|
}
|
||||||
|
return uv;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Find and reuse, or create if it does not exist, an upvalue
|
||||||
|
** at the given level.
|
||||||
|
*/
|
||||||
|
UpVal *luaF_findupval (lua_State *L, StkId level) {
|
||||||
|
UpVal **pp = &L->openupval;
|
||||||
|
UpVal *p;
|
||||||
|
lua_assert(isintwups(L) || L->openupval == NULL);
|
||||||
|
while ((p = *pp) != NULL && uplevel(p) >= level) { /* search for it */
|
||||||
|
lua_assert(!isdead(G(L), p));
|
||||||
|
if (uplevel(p) == level) /* corresponding upvalue? */
|
||||||
|
return p; /* return it */
|
||||||
|
pp = &p->u.open.next;
|
||||||
|
}
|
||||||
|
/* not found: create a new upvalue after 'pp' */
|
||||||
|
return newupval(L, level, pp);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Call closing method for object 'obj' with error message 'err'. The
|
||||||
|
** boolean 'yy' controls whether the call is yieldable.
|
||||||
|
** (This function assumes EXTRA_STACK.)
|
||||||
|
*/
|
||||||
|
static void callclosemethod (lua_State *L, TValue *obj, TValue *err, int yy) {
|
||||||
|
StkId top = L->top.p;
|
||||||
|
const TValue *tm = luaT_gettmbyobj(L, obj, TM_CLOSE);
|
||||||
|
setobj2s(L, top, tm); /* will call metamethod... */
|
||||||
|
setobj2s(L, top + 1, obj); /* with 'self' as the 1st argument */
|
||||||
|
setobj2s(L, top + 2, err); /* and error msg. as 2nd argument */
|
||||||
|
L->top.p = top + 3; /* add function and arguments */
|
||||||
|
if (yy)
|
||||||
|
luaD_call(L, top, 0);
|
||||||
|
else
|
||||||
|
luaD_callnoyield(L, top, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Check whether object at given level has a close metamethod and raise
|
||||||
|
** an error if not.
|
||||||
|
*/
|
||||||
|
static void checkclosemth (lua_State *L, StkId level) {
|
||||||
|
const TValue *tm = luaT_gettmbyobj(L, s2v(level), TM_CLOSE);
|
||||||
|
if (ttisnil(tm)) { /* no metamethod? */
|
||||||
|
int idx = cast_int(level - L->ci->func.p); /* variable index */
|
||||||
|
const char *vname = luaG_findlocal(L, L->ci, idx, NULL);
|
||||||
|
if (vname == NULL) vname = "?";
|
||||||
|
luaG_runerror(L, "variable '%s' got a non-closable value", vname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Prepare and call a closing method.
|
||||||
|
** If status is CLOSEKTOP, the call to the closing method will be pushed
|
||||||
|
** at the top of the stack. Otherwise, values can be pushed right after
|
||||||
|
** the 'level' of the upvalue being closed, as everything after that
|
||||||
|
** won't be used again.
|
||||||
|
*/
|
||||||
|
static void prepcallclosemth (lua_State *L, StkId level, int status, int yy) {
|
||||||
|
TValue *uv = s2v(level); /* value being closed */
|
||||||
|
TValue *errobj;
|
||||||
|
if (status == CLOSEKTOP)
|
||||||
|
errobj = &G(L)->nilvalue; /* error object is nil */
|
||||||
|
else { /* 'luaD_seterrorobj' will set top to level + 2 */
|
||||||
|
errobj = s2v(level + 1); /* error object goes after 'uv' */
|
||||||
|
luaD_seterrorobj(L, status, level + 1); /* set error object */
|
||||||
|
}
|
||||||
|
callclosemethod(L, uv, errobj, yy);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Maximum value for deltas in 'tbclist', dependent on the type
|
||||||
|
** of delta. (This macro assumes that an 'L' is in scope where it
|
||||||
|
** is used.)
|
||||||
|
*/
|
||||||
|
#define MAXDELTA \
|
||||||
|
((256ul << ((sizeof(L->stack.p->tbclist.delta) - 1) * 8)) - 1)
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Insert a variable in the list of to-be-closed variables.
|
||||||
|
*/
|
||||||
|
void luaF_newtbcupval (lua_State *L, StkId level) {
|
||||||
|
lua_assert(level > L->tbclist.p);
|
||||||
|
if (l_isfalse(s2v(level)))
|
||||||
|
return; /* false doesn't need to be closed */
|
||||||
|
checkclosemth(L, level); /* value must have a close method */
|
||||||
|
while (cast_uint(level - L->tbclist.p) > MAXDELTA) {
|
||||||
|
L->tbclist.p += MAXDELTA; /* create a dummy node at maximum delta */
|
||||||
|
L->tbclist.p->tbclist.delta = 0;
|
||||||
|
}
|
||||||
|
level->tbclist.delta = cast(unsigned short, level - L->tbclist.p);
|
||||||
|
L->tbclist.p = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void luaF_unlinkupval (UpVal *uv) {
|
||||||
|
lua_assert(upisopen(uv));
|
||||||
|
*uv->u.open.previous = uv->u.open.next;
|
||||||
|
if (uv->u.open.next)
|
||||||
|
uv->u.open.next->u.open.previous = uv->u.open.previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Close all upvalues up to the given stack level.
|
||||||
|
*/
|
||||||
|
void luaF_closeupval (lua_State *L, StkId level) {
|
||||||
|
UpVal *uv;
|
||||||
|
StkId upl; /* stack index pointed by 'uv' */
|
||||||
|
while ((uv = L->openupval) != NULL && (upl = uplevel(uv)) >= level) {
|
||||||
|
TValue *slot = &uv->u.value; /* new position for value */
|
||||||
|
lua_assert(uplevel(uv) < L->top.p);
|
||||||
|
luaF_unlinkupval(uv); /* remove upvalue from 'openupval' list */
|
||||||
|
setobj(L, slot, uv->v.p); /* move value to upvalue slot */
|
||||||
|
uv->v.p = slot; /* now current value lives here */
|
||||||
|
if (!iswhite(uv)) { /* neither white nor dead? */
|
||||||
|
nw2black(uv); /* closed upvalues cannot be gray */
|
||||||
|
luaC_barrier(L, uv, slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Remove first element from the tbclist plus its dummy nodes.
|
||||||
|
*/
|
||||||
|
static void poptbclist (lua_State *L) {
|
||||||
|
StkId tbc = L->tbclist.p;
|
||||||
|
lua_assert(tbc->tbclist.delta > 0); /* first element cannot be dummy */
|
||||||
|
tbc -= tbc->tbclist.delta;
|
||||||
|
while (tbc > L->stack.p && tbc->tbclist.delta == 0)
|
||||||
|
tbc -= MAXDELTA; /* remove dummy nodes */
|
||||||
|
L->tbclist.p = tbc;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Close all upvalues and to-be-closed variables up to the given stack
|
||||||
|
** level. Return restored 'level'.
|
||||||
|
*/
|
||||||
|
StkId luaF_close (lua_State *L, StkId level, int status, int yy) {
|
||||||
|
ptrdiff_t levelrel = savestack(L, level);
|
||||||
|
luaF_closeupval(L, level); /* first, close the upvalues */
|
||||||
|
while (L->tbclist.p >= level) { /* traverse tbc's down to that level */
|
||||||
|
StkId tbc = L->tbclist.p; /* get variable index */
|
||||||
|
poptbclist(L); /* remove it from list */
|
||||||
|
prepcallclosemth(L, tbc, status, yy); /* close variable */
|
||||||
|
level = restorestack(L, levelrel);
|
||||||
|
}
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Proto *luaF_newproto (lua_State *L) {
|
||||||
|
GCObject *o = luaC_newobj(L, LUA_VPROTO, sizeof(Proto));
|
||||||
|
Proto *f = gco2p(o);
|
||||||
|
f->k = NULL;
|
||||||
|
f->sizek = 0;
|
||||||
|
f->p = NULL;
|
||||||
|
f->sizep = 0;
|
||||||
|
f->code = NULL;
|
||||||
|
f->sizecode = 0;
|
||||||
|
f->lineinfo = NULL;
|
||||||
|
f->sizelineinfo = 0;
|
||||||
|
f->abslineinfo = NULL;
|
||||||
|
f->sizeabslineinfo = 0;
|
||||||
|
f->upvalues = NULL;
|
||||||
|
f->sizeupvalues = 0;
|
||||||
|
f->numparams = 0;
|
||||||
|
f->is_vararg = 0;
|
||||||
|
f->maxstacksize = 0;
|
||||||
|
f->locvars = NULL;
|
||||||
|
f->sizelocvars = 0;
|
||||||
|
f->linedefined = 0;
|
||||||
|
f->lastlinedefined = 0;
|
||||||
|
f->source = NULL;
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void luaF_freeproto (lua_State *L, Proto *f) {
|
||||||
|
luaM_freearray(L, f->code, f->sizecode);
|
||||||
|
luaM_freearray(L, f->p, f->sizep);
|
||||||
|
luaM_freearray(L, f->k, f->sizek);
|
||||||
|
luaM_freearray(L, f->lineinfo, f->sizelineinfo);
|
||||||
|
luaM_freearray(L, f->abslineinfo, f->sizeabslineinfo);
|
||||||
|
luaM_freearray(L, f->locvars, f->sizelocvars);
|
||||||
|
luaM_freearray(L, f->upvalues, f->sizeupvalues);
|
||||||
|
luaM_free(L, f);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Look for n-th local variable at line 'line' in function 'func'.
|
||||||
|
** Returns NULL if not found.
|
||||||
|
*/
|
||||||
|
const char *luaF_getlocalname (const Proto *f, int local_number, int pc) {
|
||||||
|
int i;
|
||||||
|
for (i = 0; i<f->sizelocvars && f->locvars[i].startpc <= pc; i++) {
|
||||||
|
if (pc < f->locvars[i].endpc) { /* is variable active? */
|
||||||
|
local_number--;
|
||||||
|
if (local_number == 0)
|
||||||
|
return getstr(f->locvars[i].varname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL; /* not found */
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
** $Id: lfunc.h $
|
||||||
|
** Auxiliary functions to manipulate prototypes and closures
|
||||||
|
** See Copyright Notice in lua.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef lfunc_h
|
||||||
|
#define lfunc_h
|
||||||
|
|
||||||
|
|
||||||
|
#include "lobject.h"
|
||||||
|
|
||||||
|
|
||||||
|
#define sizeCclosure(n) (cast_int(offsetof(CClosure, upvalue)) + \
|
||||||
|
cast_int(sizeof(TValue)) * (n))
|
||||||
|
|
||||||
|
#define sizeLclosure(n) (cast_int(offsetof(LClosure, upvals)) + \
|
||||||
|
cast_int(sizeof(TValue *)) * (n))
|
||||||
|
|
||||||
|
|
||||||
|
/* test whether thread is in 'twups' list */
|
||||||
|
#define isintwups(L) (L->twups != L)
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** maximum number of upvalues in a closure (both C and Lua). (Value
|
||||||
|
** must fit in a VM register.)
|
||||||
|
*/
|
||||||
|
#define MAXUPVAL 255
|
||||||
|
|
||||||
|
|
||||||
|
#define upisopen(up) ((up)->v.p != &(up)->u.value)
|
||||||
|
|
||||||
|
|
||||||
|
#define uplevel(up) check_exp(upisopen(up), cast(StkId, (up)->v.p))
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** maximum number of misses before giving up the cache of closures
|
||||||
|
** in prototypes
|
||||||
|
*/
|
||||||
|
#define MAXMISS 10
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* special status to close upvalues preserving the top of the stack */
|
||||||
|
#define CLOSEKTOP (-1)
|
||||||
|
|
||||||
|
|
||||||
|
LUAI_FUNC Proto *luaF_newproto (lua_State *L);
|
||||||
|
LUAI_FUNC CClosure *luaF_newCclosure (lua_State *L, int nupvals);
|
||||||
|
LUAI_FUNC LClosure *luaF_newLclosure (lua_State *L, int nupvals);
|
||||||
|
LUAI_FUNC void luaF_initupvals (lua_State *L, LClosure *cl);
|
||||||
|
LUAI_FUNC UpVal *luaF_findupval (lua_State *L, StkId level);
|
||||||
|
LUAI_FUNC void luaF_newtbcupval (lua_State *L, StkId level);
|
||||||
|
LUAI_FUNC void luaF_closeupval (lua_State *L, StkId level);
|
||||||
|
LUAI_FUNC StkId luaF_close (lua_State *L, StkId level, int status, int yy);
|
||||||
|
LUAI_FUNC void luaF_unlinkupval (UpVal *uv);
|
||||||
|
LUAI_FUNC void luaF_freeproto (lua_State *L, Proto *f);
|
||||||
|
LUAI_FUNC const char *luaF_getlocalname (const Proto *func, int local_number,
|
||||||
|
int pc);
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,202 @@
|
|||||||
|
/*
|
||||||
|
** $Id: lgc.h $
|
||||||
|
** Garbage Collector
|
||||||
|
** See Copyright Notice in lua.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef lgc_h
|
||||||
|
#define lgc_h
|
||||||
|
|
||||||
|
|
||||||
|
#include "lobject.h"
|
||||||
|
#include "lstate.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Collectable objects may have one of three colors: white, which means
|
||||||
|
** the object is not marked; gray, which means the object is marked, but
|
||||||
|
** its references may be not marked; and black, which means that the
|
||||||
|
** object and all its references are marked. The main invariant of the
|
||||||
|
** garbage collector, while marking objects, is that a black object can
|
||||||
|
** never point to a white one. Moreover, any gray object must be in a
|
||||||
|
** "gray list" (gray, grayagain, weak, allweak, ephemeron) so that it
|
||||||
|
** can be visited again before finishing the collection cycle. (Open
|
||||||
|
** upvalues are an exception to this rule.) These lists have no meaning
|
||||||
|
** when the invariant is not being enforced (e.g., sweep phase).
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Possible states of the Garbage Collector
|
||||||
|
*/
|
||||||
|
#define GCSpropagate 0
|
||||||
|
#define GCSenteratomic 1
|
||||||
|
#define GCSatomic 2
|
||||||
|
#define GCSswpallgc 3
|
||||||
|
#define GCSswpfinobj 4
|
||||||
|
#define GCSswptobefnz 5
|
||||||
|
#define GCSswpend 6
|
||||||
|
#define GCScallfin 7
|
||||||
|
#define GCSpause 8
|
||||||
|
|
||||||
|
|
||||||
|
#define issweepphase(g) \
|
||||||
|
(GCSswpallgc <= (g)->gcstate && (g)->gcstate <= GCSswpend)
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** macro to tell when main invariant (white objects cannot point to black
|
||||||
|
** ones) must be kept. During a collection, the sweep
|
||||||
|
** phase may break the invariant, as objects turned white may point to
|
||||||
|
** still-black objects. The invariant is restored when sweep ends and
|
||||||
|
** all objects are white again.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define keepinvariant(g) ((g)->gcstate <= GCSatomic)
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** some useful bit tricks
|
||||||
|
*/
|
||||||
|
#define resetbits(x,m) ((x) &= cast_byte(~(m)))
|
||||||
|
#define setbits(x,m) ((x) |= (m))
|
||||||
|
#define testbits(x,m) ((x) & (m))
|
||||||
|
#define bitmask(b) (1<<(b))
|
||||||
|
#define bit2mask(b1,b2) (bitmask(b1) | bitmask(b2))
|
||||||
|
#define l_setbit(x,b) setbits(x, bitmask(b))
|
||||||
|
#define resetbit(x,b) resetbits(x, bitmask(b))
|
||||||
|
#define testbit(x,b) testbits(x, bitmask(b))
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Layout for bit use in 'marked' field. First three bits are
|
||||||
|
** used for object "age" in generational mode. Last bit is used
|
||||||
|
** by tests.
|
||||||
|
*/
|
||||||
|
#define WHITE0BIT 3 /* object is white (type 0) */
|
||||||
|
#define WHITE1BIT 4 /* object is white (type 1) */
|
||||||
|
#define BLACKBIT 5 /* object is black */
|
||||||
|
#define FINALIZEDBIT 6 /* object has been marked for finalization */
|
||||||
|
|
||||||
|
#define TESTBIT 7
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#define WHITEBITS bit2mask(WHITE0BIT, WHITE1BIT)
|
||||||
|
|
||||||
|
|
||||||
|
#define iswhite(x) testbits((x)->marked, WHITEBITS)
|
||||||
|
#define isblack(x) testbit((x)->marked, BLACKBIT)
|
||||||
|
#define isgray(x) /* neither white nor black */ \
|
||||||
|
(!testbits((x)->marked, WHITEBITS | bitmask(BLACKBIT)))
|
||||||
|
|
||||||
|
#define tofinalize(x) testbit((x)->marked, FINALIZEDBIT)
|
||||||
|
|
||||||
|
#define otherwhite(g) ((g)->currentwhite ^ WHITEBITS)
|
||||||
|
#define isdeadm(ow,m) ((m) & (ow))
|
||||||
|
#define isdead(g,v) isdeadm(otherwhite(g), (v)->marked)
|
||||||
|
|
||||||
|
#define changewhite(x) ((x)->marked ^= WHITEBITS)
|
||||||
|
#define nw2black(x) \
|
||||||
|
check_exp(!iswhite(x), l_setbit((x)->marked, BLACKBIT))
|
||||||
|
|
||||||
|
#define luaC_white(g) cast_byte((g)->currentwhite & WHITEBITS)
|
||||||
|
|
||||||
|
|
||||||
|
/* object age in generational mode */
|
||||||
|
#define G_NEW 0 /* created in current cycle */
|
||||||
|
#define G_SURVIVAL 1 /* created in previous cycle */
|
||||||
|
#define G_OLD0 2 /* marked old by frw. barrier in this cycle */
|
||||||
|
#define G_OLD1 3 /* first full cycle as old */
|
||||||
|
#define G_OLD 4 /* really old object (not to be visited) */
|
||||||
|
#define G_TOUCHED1 5 /* old object touched this cycle */
|
||||||
|
#define G_TOUCHED2 6 /* old object touched in previous cycle */
|
||||||
|
|
||||||
|
#define AGEBITS 7 /* all age bits (111) */
|
||||||
|
|
||||||
|
#define getage(o) ((o)->marked & AGEBITS)
|
||||||
|
#define setage(o,a) ((o)->marked = cast_byte(((o)->marked & (~AGEBITS)) | a))
|
||||||
|
#define isold(o) (getage(o) > G_SURVIVAL)
|
||||||
|
|
||||||
|
#define changeage(o,f,t) \
|
||||||
|
check_exp(getage(o) == (f), (o)->marked ^= ((f)^(t)))
|
||||||
|
|
||||||
|
|
||||||
|
/* Default Values for GC parameters */
|
||||||
|
#define LUAI_GENMAJORMUL 100
|
||||||
|
#define LUAI_GENMINORMUL 20
|
||||||
|
|
||||||
|
/* wait memory to double before starting new cycle */
|
||||||
|
#define LUAI_GCPAUSE 200
|
||||||
|
|
||||||
|
/*
|
||||||
|
** some gc parameters are stored divided by 4 to allow a maximum value
|
||||||
|
** up to 1023 in a 'lu_byte'.
|
||||||
|
*/
|
||||||
|
#define getgcparam(p) ((p) * 4)
|
||||||
|
#define setgcparam(p,v) ((p) = (v) / 4)
|
||||||
|
|
||||||
|
#define LUAI_GCMUL 100
|
||||||
|
|
||||||
|
/* how much to allocate before next GC step (log2) */
|
||||||
|
#define LUAI_GCSTEPSIZE 13 /* 8 KB */
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Check whether the declared GC mode is generational. While in
|
||||||
|
** generational mode, the collector can go temporarily to incremental
|
||||||
|
** mode to improve performance. This is signaled by 'g->lastatomic != 0'.
|
||||||
|
*/
|
||||||
|
#define isdecGCmodegen(g) (g->gckind == KGC_GEN || g->lastatomic != 0)
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Control when GC is running:
|
||||||
|
*/
|
||||||
|
#define GCSTPUSR 1 /* bit true when GC stopped by user */
|
||||||
|
#define GCSTPGC 2 /* bit true when GC stopped by itself */
|
||||||
|
#define GCSTPCLS 4 /* bit true when closing Lua state */
|
||||||
|
#define gcrunning(g) ((g)->gcstp == 0)
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Does one step of collection when debt becomes positive. 'pre'/'pos'
|
||||||
|
** allows some adjustments to be done only when needed. macro
|
||||||
|
** 'condchangemem' is used only for heavy tests (forcing a full
|
||||||
|
** GC cycle on every opportunity)
|
||||||
|
*/
|
||||||
|
#define luaC_condGC(L,pre,pos) \
|
||||||
|
{ if (G(L)->GCdebt > 0) { pre; luaC_step(L); pos;}; \
|
||||||
|
condchangemem(L,pre,pos); }
|
||||||
|
|
||||||
|
/* more often than not, 'pre'/'pos' are empty */
|
||||||
|
#define luaC_checkGC(L) luaC_condGC(L,(void)0,(void)0)
|
||||||
|
|
||||||
|
|
||||||
|
#define luaC_objbarrier(L,p,o) ( \
|
||||||
|
(isblack(p) && iswhite(o)) ? \
|
||||||
|
luaC_barrier_(L,obj2gco(p),obj2gco(o)) : cast_void(0))
|
||||||
|
|
||||||
|
#define luaC_barrier(L,p,v) ( \
|
||||||
|
iscollectable(v) ? luaC_objbarrier(L,p,gcvalue(v)) : cast_void(0))
|
||||||
|
|
||||||
|
#define luaC_objbarrierback(L,p,o) ( \
|
||||||
|
(isblack(p) && iswhite(o)) ? luaC_barrierback_(L,p) : cast_void(0))
|
||||||
|
|
||||||
|
#define luaC_barrierback(L,p,v) ( \
|
||||||
|
iscollectable(v) ? luaC_objbarrierback(L, p, gcvalue(v)) : cast_void(0))
|
||||||
|
|
||||||
|
LUAI_FUNC void luaC_fix (lua_State *L, GCObject *o);
|
||||||
|
LUAI_FUNC void luaC_freeallobjects (lua_State *L);
|
||||||
|
LUAI_FUNC void luaC_step (lua_State *L);
|
||||||
|
LUAI_FUNC void luaC_runtilstate (lua_State *L, int statesmask);
|
||||||
|
LUAI_FUNC void luaC_fullgc (lua_State *L, int isemergency);
|
||||||
|
LUAI_FUNC GCObject *luaC_newobj (lua_State *L, int tt, size_t sz);
|
||||||
|
LUAI_FUNC GCObject *luaC_newobjdt (lua_State *L, int tt, size_t sz,
|
||||||
|
size_t offset);
|
||||||
|
LUAI_FUNC void luaC_barrier_ (lua_State *L, GCObject *o, GCObject *v);
|
||||||
|
LUAI_FUNC void luaC_barrierback_ (lua_State *L, GCObject *o);
|
||||||
|
LUAI_FUNC void luaC_checkfinalizer (lua_State *L, GCObject *o, Table *mt);
|
||||||
|
LUAI_FUNC void luaC_changemode (lua_State *L, int newmode);
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
** $Id: linit.c $
|
||||||
|
** Initialization of libraries for lua.c and other clients
|
||||||
|
** See Copyright Notice in lua.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
#define linit_c
|
||||||
|
#define LUA_LIB
|
||||||
|
|
||||||
|
/*
|
||||||
|
** If you embed Lua in your program and need to open the standard
|
||||||
|
** libraries, call luaL_openlibs in your program. If you need a
|
||||||
|
** different set of libraries, copy this file to your project and edit
|
||||||
|
** it to suit your needs.
|
||||||
|
**
|
||||||
|
** You can also *preload* libraries, so that a later 'require' can
|
||||||
|
** open the library, which is already linked to the application.
|
||||||
|
** For that, do the following code:
|
||||||
|
**
|
||||||
|
** luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_PRELOAD_TABLE);
|
||||||
|
** lua_pushcfunction(L, luaopen_modname);
|
||||||
|
** lua_setfield(L, -2, modname);
|
||||||
|
** lua_pop(L, 1); // remove PRELOAD table
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "lprefix.h"
|
||||||
|
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#include "lua.h"
|
||||||
|
|
||||||
|
#include "lualib.h"
|
||||||
|
#include "lauxlib.h"
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** these libs are loaded by lua.c and are readily available to any Lua
|
||||||
|
** program
|
||||||
|
*/
|
||||||
|
static const luaL_Reg loadedlibs[] = {
|
||||||
|
{LUA_GNAME, luaopen_base},
|
||||||
|
{LUA_LOADLIBNAME, luaopen_package},
|
||||||
|
{LUA_COLIBNAME, luaopen_coroutine},
|
||||||
|
{LUA_TABLIBNAME, luaopen_table},
|
||||||
|
{LUA_IOLIBNAME, luaopen_io},
|
||||||
|
{LUA_OSLIBNAME, luaopen_os},
|
||||||
|
{LUA_STRLIBNAME, luaopen_string},
|
||||||
|
{LUA_MATHLIBNAME, luaopen_math},
|
||||||
|
{LUA_UTF8LIBNAME, luaopen_utf8},
|
||||||
|
{LUA_DBLIBNAME, luaopen_debug},
|
||||||
|
{NULL, NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
LUALIB_API void luaL_openlibs (lua_State *L) {
|
||||||
|
const luaL_Reg *lib;
|
||||||
|
/* "require" functions from 'loadedlibs' and set results to global table */
|
||||||
|
for (lib = loadedlibs; lib->func; lib++) {
|
||||||
|
luaL_requiref(L, lib->name, lib->func, 1);
|
||||||
|
lua_pop(L, 1); /* remove lib */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,841 @@
|
|||||||
|
/*
|
||||||
|
** $Id: liolib.c $
|
||||||
|
** Standard I/O (and system) library
|
||||||
|
** See Copyright Notice in lua.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define liolib_c
|
||||||
|
#define LUA_LIB
|
||||||
|
|
||||||
|
#include "lprefix.h"
|
||||||
|
|
||||||
|
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <locale.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "lua.h"
|
||||||
|
|
||||||
|
#include "lauxlib.h"
|
||||||
|
#include "lualib.h"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Change this macro to accept other modes for 'fopen' besides
|
||||||
|
** the standard ones.
|
||||||
|
*/
|
||||||
|
#if !defined(l_checkmode)
|
||||||
|
|
||||||
|
/* accepted extensions to 'mode' in 'fopen' */
|
||||||
|
#if !defined(L_MODEEXT)
|
||||||
|
#define L_MODEEXT "b"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Check whether 'mode' matches '[rwa]%+?[L_MODEEXT]*' */
|
||||||
|
static int l_checkmode (const char *mode) {
|
||||||
|
return (*mode != '\0' && strchr("rwa", *(mode++)) != NULL &&
|
||||||
|
(*mode != '+' || ((void)(++mode), 1)) && /* skip if char is '+' */
|
||||||
|
(strspn(mode, L_MODEEXT) == strlen(mode))); /* check extensions */
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
** {======================================================
|
||||||
|
** l_popen spawns a new process connected to the current
|
||||||
|
** one through the file streams.
|
||||||
|
** =======================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
#if !defined(l_popen) /* { */
|
||||||
|
|
||||||
|
#if defined(LUA_USE_POSIX) /* { */
|
||||||
|
|
||||||
|
#define l_popen(L,c,m) (fflush(NULL), popen(c,m))
|
||||||
|
#define l_pclose(L,file) (pclose(file))
|
||||||
|
|
||||||
|
#elif defined(LUA_USE_WINDOWS) /* }{ */
|
||||||
|
|
||||||
|
#define l_popen(L,c,m) (_popen(c,m))
|
||||||
|
#define l_pclose(L,file) (_pclose(file))
|
||||||
|
|
||||||
|
#if !defined(l_checkmodep)
|
||||||
|
/* Windows accepts "[rw][bt]?" as valid modes */
|
||||||
|
#define l_checkmodep(m) ((m[0] == 'r' || m[0] == 'w') && \
|
||||||
|
(m[1] == '\0' || ((m[1] == 'b' || m[1] == 't') && m[2] == '\0')))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#else /* }{ */
|
||||||
|
|
||||||
|
/* ISO C definitions */
|
||||||
|
#define l_popen(L,c,m) \
|
||||||
|
((void)c, (void)m, \
|
||||||
|
luaL_error(L, "'popen' not supported"), \
|
||||||
|
(FILE*)0)
|
||||||
|
#define l_pclose(L,file) ((void)L, (void)file, -1)
|
||||||
|
|
||||||
|
#endif /* } */
|
||||||
|
|
||||||
|
#endif /* } */
|
||||||
|
|
||||||
|
|
||||||
|
#if !defined(l_checkmodep)
|
||||||
|
/* By default, Lua accepts only "r" or "w" as valid modes */
|
||||||
|
#define l_checkmodep(m) ((m[0] == 'r' || m[0] == 'w') && m[1] == '\0')
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* }====================================================== */
|
||||||
|
|
||||||
|
|
||||||
|
#if !defined(l_getc) /* { */
|
||||||
|
|
||||||
|
#if defined(LUA_USE_POSIX)
|
||||||
|
#define l_getc(f) getc_unlocked(f)
|
||||||
|
#define l_lockfile(f) flockfile(f)
|
||||||
|
#define l_unlockfile(f) funlockfile(f)
|
||||||
|
#else
|
||||||
|
#define l_getc(f) getc(f)
|
||||||
|
#define l_lockfile(f) ((void)0)
|
||||||
|
#define l_unlockfile(f) ((void)0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* } */
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** {======================================================
|
||||||
|
** l_fseek: configuration for longer offsets
|
||||||
|
** =======================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
#if !defined(l_fseek) /* { */
|
||||||
|
|
||||||
|
#if defined(LUA_USE_POSIX) /* { */
|
||||||
|
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
#define l_fseek(f,o,w) fseeko(f,o,w)
|
||||||
|
#define l_ftell(f) ftello(f)
|
||||||
|
#define l_seeknum off_t
|
||||||
|
|
||||||
|
#elif defined(LUA_USE_WINDOWS) && !defined(_CRTIMP_TYPEINFO) \
|
||||||
|
&& defined(_MSC_VER) && (_MSC_VER >= 1400) /* }{ */
|
||||||
|
|
||||||
|
/* Windows (but not DDK) and Visual C++ 2005 or higher */
|
||||||
|
#define l_fseek(f,o,w) _fseeki64(f,o,w)
|
||||||
|
#define l_ftell(f) _ftelli64(f)
|
||||||
|
#define l_seeknum __int64
|
||||||
|
|
||||||
|
#else /* }{ */
|
||||||
|
|
||||||
|
/* ISO C definitions */
|
||||||
|
#define l_fseek(f,o,w) fseek(f,o,w)
|
||||||
|
#define l_ftell(f) ftell(f)
|
||||||
|
#define l_seeknum long
|
||||||
|
|
||||||
|
#endif /* } */
|
||||||
|
|
||||||
|
#endif /* } */
|
||||||
|
|
||||||
|
/* }====================================================== */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#define IO_PREFIX "_IO_"
|
||||||
|
#define IOPREF_LEN (sizeof(IO_PREFIX)/sizeof(char) - 1)
|
||||||
|
#define IO_INPUT (IO_PREFIX "input")
|
||||||
|
#define IO_OUTPUT (IO_PREFIX "output")
|
||||||
|
|
||||||
|
|
||||||
|
typedef luaL_Stream LStream;
|
||||||
|
|
||||||
|
|
||||||
|
#define tolstream(L) ((LStream *)luaL_checkudata(L, 1, LUA_FILEHANDLE))
|
||||||
|
|
||||||
|
#define isclosed(p) ((p)->closef == NULL)
|
||||||
|
|
||||||
|
|
||||||
|
static int io_type (lua_State *L) {
|
||||||
|
LStream *p;
|
||||||
|
luaL_checkany(L, 1);
|
||||||
|
p = (LStream *)luaL_testudata(L, 1, LUA_FILEHANDLE);
|
||||||
|
if (p == NULL)
|
||||||
|
luaL_pushfail(L); /* not a file */
|
||||||
|
else if (isclosed(p))
|
||||||
|
lua_pushliteral(L, "closed file");
|
||||||
|
else
|
||||||
|
lua_pushliteral(L, "file");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int f_tostring (lua_State *L) {
|
||||||
|
LStream *p = tolstream(L);
|
||||||
|
if (isclosed(p))
|
||||||
|
lua_pushliteral(L, "file (closed)");
|
||||||
|
else
|
||||||
|
lua_pushfstring(L, "file (%p)", p->f);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static FILE *tofile (lua_State *L) {
|
||||||
|
LStream *p = tolstream(L);
|
||||||
|
if (l_unlikely(isclosed(p)))
|
||||||
|
luaL_error(L, "attempt to use a closed file");
|
||||||
|
lua_assert(p->f);
|
||||||
|
return p->f;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** When creating file handles, always creates a 'closed' file handle
|
||||||
|
** before opening the actual file; so, if there is a memory error, the
|
||||||
|
** handle is in a consistent state.
|
||||||
|
*/
|
||||||
|
static LStream *newprefile (lua_State *L) {
|
||||||
|
LStream *p = (LStream *)lua_newuserdatauv(L, sizeof(LStream), 0);
|
||||||
|
p->closef = NULL; /* mark file handle as 'closed' */
|
||||||
|
luaL_setmetatable(L, LUA_FILEHANDLE);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Calls the 'close' function from a file handle. The 'volatile' avoids
|
||||||
|
** a bug in some versions of the Clang compiler (e.g., clang 3.0 for
|
||||||
|
** 32 bits).
|
||||||
|
*/
|
||||||
|
static int aux_close (lua_State *L) {
|
||||||
|
LStream *p = tolstream(L);
|
||||||
|
volatile lua_CFunction cf = p->closef;
|
||||||
|
p->closef = NULL; /* mark stream as closed */
|
||||||
|
return (*cf)(L); /* close it */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int f_close (lua_State *L) {
|
||||||
|
tofile(L); /* make sure argument is an open stream */
|
||||||
|
return aux_close(L);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int io_close (lua_State *L) {
|
||||||
|
if (lua_isnone(L, 1)) /* no argument? */
|
||||||
|
lua_getfield(L, LUA_REGISTRYINDEX, IO_OUTPUT); /* use default output */
|
||||||
|
return f_close(L);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int f_gc (lua_State *L) {
|
||||||
|
LStream *p = tolstream(L);
|
||||||
|
if (!isclosed(p) && p->f != NULL)
|
||||||
|
aux_close(L); /* ignore closed and incompletely open files */
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** function to close regular files
|
||||||
|
*/
|
||||||
|
static int io_fclose (lua_State *L) {
|
||||||
|
LStream *p = tolstream(L);
|
||||||
|
errno = 0;
|
||||||
|
return luaL_fileresult(L, (fclose(p->f) == 0), NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static LStream *newfile (lua_State *L) {
|
||||||
|
LStream *p = newprefile(L);
|
||||||
|
p->f = NULL;
|
||||||
|
p->closef = &io_fclose;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void opencheck (lua_State *L, const char *fname, const char *mode) {
|
||||||
|
LStream *p = newfile(L);
|
||||||
|
p->f = fopen(fname, mode);
|
||||||
|
if (l_unlikely(p->f == NULL))
|
||||||
|
luaL_error(L, "cannot open file '%s' (%s)", fname, strerror(errno));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int io_open (lua_State *L) {
|
||||||
|
const char *filename = luaL_checkstring(L, 1);
|
||||||
|
const char *mode = luaL_optstring(L, 2, "r");
|
||||||
|
LStream *p = newfile(L);
|
||||||
|
const char *md = mode; /* to traverse/check mode */
|
||||||
|
luaL_argcheck(L, l_checkmode(md), 2, "invalid mode");
|
||||||
|
errno = 0;
|
||||||
|
p->f = fopen(filename, mode);
|
||||||
|
return (p->f == NULL) ? luaL_fileresult(L, 0, filename) : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** function to close 'popen' files
|
||||||
|
*/
|
||||||
|
static int io_pclose (lua_State *L) {
|
||||||
|
LStream *p = tolstream(L);
|
||||||
|
errno = 0;
|
||||||
|
return luaL_execresult(L, l_pclose(L, p->f));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int io_popen (lua_State *L) {
|
||||||
|
const char *filename = luaL_checkstring(L, 1);
|
||||||
|
const char *mode = luaL_optstring(L, 2, "r");
|
||||||
|
LStream *p = newprefile(L);
|
||||||
|
luaL_argcheck(L, l_checkmodep(mode), 2, "invalid mode");
|
||||||
|
errno = 0;
|
||||||
|
p->f = l_popen(L, filename, mode);
|
||||||
|
p->closef = &io_pclose;
|
||||||
|
return (p->f == NULL) ? luaL_fileresult(L, 0, filename) : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int io_tmpfile (lua_State *L) {
|
||||||
|
LStream *p = newfile(L);
|
||||||
|
errno = 0;
|
||||||
|
p->f = tmpfile();
|
||||||
|
return (p->f == NULL) ? luaL_fileresult(L, 0, NULL) : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static FILE *getiofile (lua_State *L, const char *findex) {
|
||||||
|
LStream *p;
|
||||||
|
lua_getfield(L, LUA_REGISTRYINDEX, findex);
|
||||||
|
p = (LStream *)lua_touserdata(L, -1);
|
||||||
|
if (l_unlikely(isclosed(p)))
|
||||||
|
luaL_error(L, "default %s file is closed", findex + IOPREF_LEN);
|
||||||
|
return p->f;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int g_iofile (lua_State *L, const char *f, const char *mode) {
|
||||||
|
if (!lua_isnoneornil(L, 1)) {
|
||||||
|
const char *filename = lua_tostring(L, 1);
|
||||||
|
if (filename)
|
||||||
|
opencheck(L, filename, mode);
|
||||||
|
else {
|
||||||
|
tofile(L); /* check that it's a valid file handle */
|
||||||
|
lua_pushvalue(L, 1);
|
||||||
|
}
|
||||||
|
lua_setfield(L, LUA_REGISTRYINDEX, f);
|
||||||
|
}
|
||||||
|
/* return current value */
|
||||||
|
lua_getfield(L, LUA_REGISTRYINDEX, f);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int io_input (lua_State *L) {
|
||||||
|
return g_iofile(L, IO_INPUT, "r");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int io_output (lua_State *L) {
|
||||||
|
return g_iofile(L, IO_OUTPUT, "w");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int io_readline (lua_State *L);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** maximum number of arguments to 'f:lines'/'io.lines' (it + 3 must fit
|
||||||
|
** in the limit for upvalues of a closure)
|
||||||
|
*/
|
||||||
|
#define MAXARGLINE 250
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Auxiliary function to create the iteration function for 'lines'.
|
||||||
|
** The iteration function is a closure over 'io_readline', with
|
||||||
|
** the following upvalues:
|
||||||
|
** 1) The file being read (first value in the stack)
|
||||||
|
** 2) the number of arguments to read
|
||||||
|
** 3) a boolean, true iff file has to be closed when finished ('toclose')
|
||||||
|
** *) a variable number of format arguments (rest of the stack)
|
||||||
|
*/
|
||||||
|
static void aux_lines (lua_State *L, int toclose) {
|
||||||
|
int n = lua_gettop(L) - 1; /* number of arguments to read */
|
||||||
|
luaL_argcheck(L, n <= MAXARGLINE, MAXARGLINE + 2, "too many arguments");
|
||||||
|
lua_pushvalue(L, 1); /* file */
|
||||||
|
lua_pushinteger(L, n); /* number of arguments to read */
|
||||||
|
lua_pushboolean(L, toclose); /* close/not close file when finished */
|
||||||
|
lua_rotate(L, 2, 3); /* move the three values to their positions */
|
||||||
|
lua_pushcclosure(L, io_readline, 3 + n);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int f_lines (lua_State *L) {
|
||||||
|
tofile(L); /* check that it's a valid file handle */
|
||||||
|
aux_lines(L, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Return an iteration function for 'io.lines'. If file has to be
|
||||||
|
** closed, also returns the file itself as a second result (to be
|
||||||
|
** closed as the state at the exit of a generic for).
|
||||||
|
*/
|
||||||
|
static int io_lines (lua_State *L) {
|
||||||
|
int toclose;
|
||||||
|
if (lua_isnone(L, 1)) lua_pushnil(L); /* at least one argument */
|
||||||
|
if (lua_isnil(L, 1)) { /* no file name? */
|
||||||
|
lua_getfield(L, LUA_REGISTRYINDEX, IO_INPUT); /* get default input */
|
||||||
|
lua_replace(L, 1); /* put it at index 1 */
|
||||||
|
tofile(L); /* check that it's a valid file handle */
|
||||||
|
toclose = 0; /* do not close it after iteration */
|
||||||
|
}
|
||||||
|
else { /* open a new file */
|
||||||
|
const char *filename = luaL_checkstring(L, 1);
|
||||||
|
opencheck(L, filename, "r");
|
||||||
|
lua_replace(L, 1); /* put file at index 1 */
|
||||||
|
toclose = 1; /* close it after iteration */
|
||||||
|
}
|
||||||
|
aux_lines(L, toclose); /* push iteration function */
|
||||||
|
if (toclose) {
|
||||||
|
lua_pushnil(L); /* state */
|
||||||
|
lua_pushnil(L); /* control */
|
||||||
|
lua_pushvalue(L, 1); /* file is the to-be-closed variable (4th result) */
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** {======================================================
|
||||||
|
** READ
|
||||||
|
** =======================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/* maximum length of a numeral */
|
||||||
|
#if !defined (L_MAXLENNUM)
|
||||||
|
#define L_MAXLENNUM 200
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/* auxiliary structure used by 'read_number' */
|
||||||
|
typedef struct {
|
||||||
|
FILE *f; /* file being read */
|
||||||
|
int c; /* current character (look ahead) */
|
||||||
|
int n; /* number of elements in buffer 'buff' */
|
||||||
|
char buff[L_MAXLENNUM + 1]; /* +1 for ending '\0' */
|
||||||
|
} RN;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Add current char to buffer (if not out of space) and read next one
|
||||||
|
*/
|
||||||
|
static int nextc (RN *rn) {
|
||||||
|
if (l_unlikely(rn->n >= L_MAXLENNUM)) { /* buffer overflow? */
|
||||||
|
rn->buff[0] = '\0'; /* invalidate result */
|
||||||
|
return 0; /* fail */
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
rn->buff[rn->n++] = rn->c; /* save current char */
|
||||||
|
rn->c = l_getc(rn->f); /* read next one */
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Accept current char if it is in 'set' (of size 2)
|
||||||
|
*/
|
||||||
|
static int test2 (RN *rn, const char *set) {
|
||||||
|
if (rn->c == set[0] || rn->c == set[1])
|
||||||
|
return nextc(rn);
|
||||||
|
else return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Read a sequence of (hex)digits
|
||||||
|
*/
|
||||||
|
static int readdigits (RN *rn, int hex) {
|
||||||
|
int count = 0;
|
||||||
|
while ((hex ? isxdigit(rn->c) : isdigit(rn->c)) && nextc(rn))
|
||||||
|
count++;
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Read a number: first reads a valid prefix of a numeral into a buffer.
|
||||||
|
** Then it calls 'lua_stringtonumber' to check whether the format is
|
||||||
|
** correct and to convert it to a Lua number.
|
||||||
|
*/
|
||||||
|
static int read_number (lua_State *L, FILE *f) {
|
||||||
|
RN rn;
|
||||||
|
int count = 0;
|
||||||
|
int hex = 0;
|
||||||
|
char decp[2];
|
||||||
|
rn.f = f; rn.n = 0;
|
||||||
|
decp[0] = lua_getlocaledecpoint(); /* get decimal point from locale */
|
||||||
|
decp[1] = '.'; /* always accept a dot */
|
||||||
|
l_lockfile(rn.f);
|
||||||
|
do { rn.c = l_getc(rn.f); } while (isspace(rn.c)); /* skip spaces */
|
||||||
|
test2(&rn, "-+"); /* optional sign */
|
||||||
|
if (test2(&rn, "00")) {
|
||||||
|
if (test2(&rn, "xX")) hex = 1; /* numeral is hexadecimal */
|
||||||
|
else count = 1; /* count initial '0' as a valid digit */
|
||||||
|
}
|
||||||
|
count += readdigits(&rn, hex); /* integral part */
|
||||||
|
if (test2(&rn, decp)) /* decimal point? */
|
||||||
|
count += readdigits(&rn, hex); /* fractional part */
|
||||||
|
if (count > 0 && test2(&rn, (hex ? "pP" : "eE"))) { /* exponent mark? */
|
||||||
|
test2(&rn, "-+"); /* exponent sign */
|
||||||
|
readdigits(&rn, 0); /* exponent digits */
|
||||||
|
}
|
||||||
|
ungetc(rn.c, rn.f); /* unread look-ahead char */
|
||||||
|
l_unlockfile(rn.f);
|
||||||
|
rn.buff[rn.n] = '\0'; /* finish string */
|
||||||
|
if (l_likely(lua_stringtonumber(L, rn.buff)))
|
||||||
|
return 1; /* ok, it is a valid number */
|
||||||
|
else { /* invalid format */
|
||||||
|
lua_pushnil(L); /* "result" to be removed */
|
||||||
|
return 0; /* read fails */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int test_eof (lua_State *L, FILE *f) {
|
||||||
|
int c = getc(f);
|
||||||
|
ungetc(c, f); /* no-op when c == EOF */
|
||||||
|
lua_pushliteral(L, "");
|
||||||
|
return (c != EOF);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int read_line (lua_State *L, FILE *f, int chop) {
|
||||||
|
luaL_Buffer b;
|
||||||
|
int c;
|
||||||
|
luaL_buffinit(L, &b);
|
||||||
|
do { /* may need to read several chunks to get whole line */
|
||||||
|
char *buff = luaL_prepbuffer(&b); /* preallocate buffer space */
|
||||||
|
int i = 0;
|
||||||
|
l_lockfile(f); /* no memory errors can happen inside the lock */
|
||||||
|
while (i < LUAL_BUFFERSIZE && (c = l_getc(f)) != EOF && c != '\n')
|
||||||
|
buff[i++] = c; /* read up to end of line or buffer limit */
|
||||||
|
l_unlockfile(f);
|
||||||
|
luaL_addsize(&b, i);
|
||||||
|
} while (c != EOF && c != '\n'); /* repeat until end of line */
|
||||||
|
if (!chop && c == '\n') /* want a newline and have one? */
|
||||||
|
luaL_addchar(&b, c); /* add ending newline to result */
|
||||||
|
luaL_pushresult(&b); /* close buffer */
|
||||||
|
/* return ok if read something (either a newline or something else) */
|
||||||
|
return (c == '\n' || lua_rawlen(L, -1) > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void read_all (lua_State *L, FILE *f) {
|
||||||
|
size_t nr;
|
||||||
|
luaL_Buffer b;
|
||||||
|
luaL_buffinit(L, &b);
|
||||||
|
do { /* read file in chunks of LUAL_BUFFERSIZE bytes */
|
||||||
|
char *p = luaL_prepbuffer(&b);
|
||||||
|
nr = fread(p, sizeof(char), LUAL_BUFFERSIZE, f);
|
||||||
|
luaL_addsize(&b, nr);
|
||||||
|
} while (nr == LUAL_BUFFERSIZE);
|
||||||
|
luaL_pushresult(&b); /* close buffer */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int read_chars (lua_State *L, FILE *f, size_t n) {
|
||||||
|
size_t nr; /* number of chars actually read */
|
||||||
|
char *p;
|
||||||
|
luaL_Buffer b;
|
||||||
|
luaL_buffinit(L, &b);
|
||||||
|
p = luaL_prepbuffsize(&b, n); /* prepare buffer to read whole block */
|
||||||
|
nr = fread(p, sizeof(char), n, f); /* try to read 'n' chars */
|
||||||
|
luaL_addsize(&b, nr);
|
||||||
|
luaL_pushresult(&b); /* close buffer */
|
||||||
|
return (nr > 0); /* true iff read something */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int g_read (lua_State *L, FILE *f, int first) {
|
||||||
|
int nargs = lua_gettop(L) - 1;
|
||||||
|
int n, success;
|
||||||
|
clearerr(f);
|
||||||
|
errno = 0;
|
||||||
|
if (nargs == 0) { /* no arguments? */
|
||||||
|
success = read_line(L, f, 1);
|
||||||
|
n = first + 1; /* to return 1 result */
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
/* ensure stack space for all results and for auxlib's buffer */
|
||||||
|
luaL_checkstack(L, nargs+LUA_MINSTACK, "too many arguments");
|
||||||
|
success = 1;
|
||||||
|
for (n = first; nargs-- && success; n++) {
|
||||||
|
if (lua_type(L, n) == LUA_TNUMBER) {
|
||||||
|
size_t l = (size_t)luaL_checkinteger(L, n);
|
||||||
|
success = (l == 0) ? test_eof(L, f) : read_chars(L, f, l);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const char *p = luaL_checkstring(L, n);
|
||||||
|
if (*p == '*') p++; /* skip optional '*' (for compatibility) */
|
||||||
|
switch (*p) {
|
||||||
|
case 'n': /* number */
|
||||||
|
success = read_number(L, f);
|
||||||
|
break;
|
||||||
|
case 'l': /* line */
|
||||||
|
success = read_line(L, f, 1);
|
||||||
|
break;
|
||||||
|
case 'L': /* line with end-of-line */
|
||||||
|
success = read_line(L, f, 0);
|
||||||
|
break;
|
||||||
|
case 'a': /* file */
|
||||||
|
read_all(L, f); /* read entire file */
|
||||||
|
success = 1; /* always success */
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return luaL_argerror(L, n, "invalid format");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ferror(f))
|
||||||
|
return luaL_fileresult(L, 0, NULL);
|
||||||
|
if (!success) {
|
||||||
|
lua_pop(L, 1); /* remove last result */
|
||||||
|
luaL_pushfail(L); /* push nil instead */
|
||||||
|
}
|
||||||
|
return n - first;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int io_read (lua_State *L) {
|
||||||
|
return g_read(L, getiofile(L, IO_INPUT), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int f_read (lua_State *L) {
|
||||||
|
return g_read(L, tofile(L), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Iteration function for 'lines'.
|
||||||
|
*/
|
||||||
|
static int io_readline (lua_State *L) {
|
||||||
|
LStream *p = (LStream *)lua_touserdata(L, lua_upvalueindex(1));
|
||||||
|
int i;
|
||||||
|
int n = (int)lua_tointeger(L, lua_upvalueindex(2));
|
||||||
|
if (isclosed(p)) /* file is already closed? */
|
||||||
|
return luaL_error(L, "file is already closed");
|
||||||
|
lua_settop(L , 1);
|
||||||
|
luaL_checkstack(L, n, "too many arguments");
|
||||||
|
for (i = 1; i <= n; i++) /* push arguments to 'g_read' */
|
||||||
|
lua_pushvalue(L, lua_upvalueindex(3 + i));
|
||||||
|
n = g_read(L, p->f, 2); /* 'n' is number of results */
|
||||||
|
lua_assert(n > 0); /* should return at least a nil */
|
||||||
|
if (lua_toboolean(L, -n)) /* read at least one value? */
|
||||||
|
return n; /* return them */
|
||||||
|
else { /* first result is false: EOF or error */
|
||||||
|
if (n > 1) { /* is there error information? */
|
||||||
|
/* 2nd result is error message */
|
||||||
|
return luaL_error(L, "%s", lua_tostring(L, -n + 1));
|
||||||
|
}
|
||||||
|
if (lua_toboolean(L, lua_upvalueindex(3))) { /* generator created file? */
|
||||||
|
lua_settop(L, 0); /* clear stack */
|
||||||
|
lua_pushvalue(L, lua_upvalueindex(1)); /* push file at index 1 */
|
||||||
|
aux_close(L); /* close it */
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* }====================================================== */
|
||||||
|
|
||||||
|
|
||||||
|
static int g_write (lua_State *L, FILE *f, int arg) {
|
||||||
|
int nargs = lua_gettop(L) - arg;
|
||||||
|
int status = 1;
|
||||||
|
errno = 0;
|
||||||
|
for (; nargs--; arg++) {
|
||||||
|
if (lua_type(L, arg) == LUA_TNUMBER) {
|
||||||
|
/* optimization: could be done exactly as for strings */
|
||||||
|
int len = lua_isinteger(L, arg)
|
||||||
|
? fprintf(f, LUA_INTEGER_FMT,
|
||||||
|
(LUAI_UACINT)lua_tointeger(L, arg))
|
||||||
|
: fprintf(f, LUA_NUMBER_FMT,
|
||||||
|
(LUAI_UACNUMBER)lua_tonumber(L, arg));
|
||||||
|
status = status && (len > 0);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
size_t l;
|
||||||
|
const char *s = luaL_checklstring(L, arg, &l);
|
||||||
|
status = status && (fwrite(s, sizeof(char), l, f) == l);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (l_likely(status))
|
||||||
|
return 1; /* file handle already on stack top */
|
||||||
|
else
|
||||||
|
return luaL_fileresult(L, status, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int io_write (lua_State *L) {
|
||||||
|
return g_write(L, getiofile(L, IO_OUTPUT), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int f_write (lua_State *L) {
|
||||||
|
FILE *f = tofile(L);
|
||||||
|
lua_pushvalue(L, 1); /* push file at the stack top (to be returned) */
|
||||||
|
return g_write(L, f, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int f_seek (lua_State *L) {
|
||||||
|
static const int mode[] = {SEEK_SET, SEEK_CUR, SEEK_END};
|
||||||
|
static const char *const modenames[] = {"set", "cur", "end", NULL};
|
||||||
|
FILE *f = tofile(L);
|
||||||
|
int op = luaL_checkoption(L, 2, "cur", modenames);
|
||||||
|
lua_Integer p3 = luaL_optinteger(L, 3, 0);
|
||||||
|
l_seeknum offset = (l_seeknum)p3;
|
||||||
|
luaL_argcheck(L, (lua_Integer)offset == p3, 3,
|
||||||
|
"not an integer in proper range");
|
||||||
|
errno = 0;
|
||||||
|
op = l_fseek(f, offset, mode[op]);
|
||||||
|
if (l_unlikely(op))
|
||||||
|
return luaL_fileresult(L, 0, NULL); /* error */
|
||||||
|
else {
|
||||||
|
lua_pushinteger(L, (lua_Integer)l_ftell(f));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int f_setvbuf (lua_State *L) {
|
||||||
|
static const int mode[] = {_IONBF, _IOFBF, _IOLBF};
|
||||||
|
static const char *const modenames[] = {"no", "full", "line", NULL};
|
||||||
|
FILE *f = tofile(L);
|
||||||
|
int op = luaL_checkoption(L, 2, NULL, modenames);
|
||||||
|
lua_Integer sz = luaL_optinteger(L, 3, LUAL_BUFFERSIZE);
|
||||||
|
int res;
|
||||||
|
errno = 0;
|
||||||
|
res = setvbuf(f, NULL, mode[op], (size_t)sz);
|
||||||
|
return luaL_fileresult(L, res == 0, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
static int io_flush (lua_State *L) {
|
||||||
|
FILE *f = getiofile(L, IO_OUTPUT);
|
||||||
|
errno = 0;
|
||||||
|
return luaL_fileresult(L, fflush(f) == 0, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int f_flush (lua_State *L) {
|
||||||
|
FILE *f = tofile(L);
|
||||||
|
errno = 0;
|
||||||
|
return luaL_fileresult(L, fflush(f) == 0, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** functions for 'io' library
|
||||||
|
*/
|
||||||
|
static const luaL_Reg iolib[] = {
|
||||||
|
{"close", io_close},
|
||||||
|
{"flush", io_flush},
|
||||||
|
{"input", io_input},
|
||||||
|
{"lines", io_lines},
|
||||||
|
{"open", io_open},
|
||||||
|
{"output", io_output},
|
||||||
|
{"popen", io_popen},
|
||||||
|
{"read", io_read},
|
||||||
|
{"tmpfile", io_tmpfile},
|
||||||
|
{"type", io_type},
|
||||||
|
{"write", io_write},
|
||||||
|
{NULL, NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** methods for file handles
|
||||||
|
*/
|
||||||
|
static const luaL_Reg meth[] = {
|
||||||
|
{"read", f_read},
|
||||||
|
{"write", f_write},
|
||||||
|
{"lines", f_lines},
|
||||||
|
{"flush", f_flush},
|
||||||
|
{"seek", f_seek},
|
||||||
|
{"close", f_close},
|
||||||
|
{"setvbuf", f_setvbuf},
|
||||||
|
{NULL, NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** metamethods for file handles
|
||||||
|
*/
|
||||||
|
static const luaL_Reg metameth[] = {
|
||||||
|
{"__index", NULL}, /* placeholder */
|
||||||
|
{"__gc", f_gc},
|
||||||
|
{"__close", f_gc},
|
||||||
|
{"__tostring", f_tostring},
|
||||||
|
{NULL, NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
static void createmeta (lua_State *L) {
|
||||||
|
luaL_newmetatable(L, LUA_FILEHANDLE); /* metatable for file handles */
|
||||||
|
luaL_setfuncs(L, metameth, 0); /* add metamethods to new metatable */
|
||||||
|
luaL_newlibtable(L, meth); /* create method table */
|
||||||
|
luaL_setfuncs(L, meth, 0); /* add file methods to method table */
|
||||||
|
lua_setfield(L, -2, "__index"); /* metatable.__index = method table */
|
||||||
|
lua_pop(L, 1); /* pop metatable */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** function to (not) close the standard files stdin, stdout, and stderr
|
||||||
|
*/
|
||||||
|
static int io_noclose (lua_State *L) {
|
||||||
|
LStream *p = tolstream(L);
|
||||||
|
p->closef = &io_noclose; /* keep file opened */
|
||||||
|
luaL_pushfail(L);
|
||||||
|
lua_pushliteral(L, "cannot close standard file");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void createstdfile (lua_State *L, FILE *f, const char *k,
|
||||||
|
const char *fname) {
|
||||||
|
LStream *p = newprefile(L);
|
||||||
|
p->f = f;
|
||||||
|
p->closef = &io_noclose;
|
||||||
|
if (k != NULL) {
|
||||||
|
lua_pushvalue(L, -1);
|
||||||
|
lua_setfield(L, LUA_REGISTRYINDEX, k); /* add file to registry */
|
||||||
|
}
|
||||||
|
lua_setfield(L, -2, fname); /* add file to module */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
LUAMOD_API int luaopen_io (lua_State *L) {
|
||||||
|
luaL_newlib(L, iolib); /* new module */
|
||||||
|
createmeta(L);
|
||||||
|
/* create (and set) default files */
|
||||||
|
createstdfile(L, stdin, IO_INPUT, "stdin");
|
||||||
|
createstdfile(L, stdout, IO_OUTPUT, "stdout");
|
||||||
|
createstdfile(L, stderr, NULL, "stderr");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
/*
|
||||||
|
** $Id: ljumptab.h $
|
||||||
|
** Jump Table for the Lua interpreter
|
||||||
|
** See Copyright Notice in lua.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
#undef vmdispatch
|
||||||
|
#undef vmcase
|
||||||
|
#undef vmbreak
|
||||||
|
|
||||||
|
#define vmdispatch(x) goto *disptab[x];
|
||||||
|
|
||||||
|
#define vmcase(l) L_##l:
|
||||||
|
|
||||||
|
#define vmbreak vmfetch(); vmdispatch(GET_OPCODE(i));
|
||||||
|
|
||||||
|
|
||||||
|
static const void *const disptab[NUM_OPCODES] = {
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
** you can update the following list with this command:
|
||||||
|
**
|
||||||
|
** sed -n '/^OP_/\!d; s/OP_/\&\&L_OP_/ ; s/,.*/,/ ; s/\/.*// ; p' lopcodes.h
|
||||||
|
**
|
||||||
|
#endif
|
||||||
|
|
||||||
|
&&L_OP_MOVE,
|
||||||
|
&&L_OP_LOADI,
|
||||||
|
&&L_OP_LOADF,
|
||||||
|
&&L_OP_LOADK,
|
||||||
|
&&L_OP_LOADKX,
|
||||||
|
&&L_OP_LOADFALSE,
|
||||||
|
&&L_OP_LFALSESKIP,
|
||||||
|
&&L_OP_LOADTRUE,
|
||||||
|
&&L_OP_LOADNIL,
|
||||||
|
&&L_OP_GETUPVAL,
|
||||||
|
&&L_OP_SETUPVAL,
|
||||||
|
&&L_OP_GETTABUP,
|
||||||
|
&&L_OP_GETTABLE,
|
||||||
|
&&L_OP_GETI,
|
||||||
|
&&L_OP_GETFIELD,
|
||||||
|
&&L_OP_SETTABUP,
|
||||||
|
&&L_OP_SETTABLE,
|
||||||
|
&&L_OP_SETI,
|
||||||
|
&&L_OP_SETFIELD,
|
||||||
|
&&L_OP_NEWTABLE,
|
||||||
|
&&L_OP_SELF,
|
||||||
|
&&L_OP_ADDI,
|
||||||
|
&&L_OP_ADDK,
|
||||||
|
&&L_OP_SUBK,
|
||||||
|
&&L_OP_MULK,
|
||||||
|
&&L_OP_MODK,
|
||||||
|
&&L_OP_POWK,
|
||||||
|
&&L_OP_DIVK,
|
||||||
|
&&L_OP_IDIVK,
|
||||||
|
&&L_OP_BANDK,
|
||||||
|
&&L_OP_BORK,
|
||||||
|
&&L_OP_BXORK,
|
||||||
|
&&L_OP_SHRI,
|
||||||
|
&&L_OP_SHLI,
|
||||||
|
&&L_OP_ADD,
|
||||||
|
&&L_OP_SUB,
|
||||||
|
&&L_OP_MUL,
|
||||||
|
&&L_OP_MOD,
|
||||||
|
&&L_OP_POW,
|
||||||
|
&&L_OP_DIV,
|
||||||
|
&&L_OP_IDIV,
|
||||||
|
&&L_OP_BAND,
|
||||||
|
&&L_OP_BOR,
|
||||||
|
&&L_OP_BXOR,
|
||||||
|
&&L_OP_SHL,
|
||||||
|
&&L_OP_SHR,
|
||||||
|
&&L_OP_MMBIN,
|
||||||
|
&&L_OP_MMBINI,
|
||||||
|
&&L_OP_MMBINK,
|
||||||
|
&&L_OP_UNM,
|
||||||
|
&&L_OP_BNOT,
|
||||||
|
&&L_OP_NOT,
|
||||||
|
&&L_OP_LEN,
|
||||||
|
&&L_OP_CONCAT,
|
||||||
|
&&L_OP_CLOSE,
|
||||||
|
&&L_OP_TBC,
|
||||||
|
&&L_OP_JMP,
|
||||||
|
&&L_OP_EQ,
|
||||||
|
&&L_OP_LT,
|
||||||
|
&&L_OP_LE,
|
||||||
|
&&L_OP_EQK,
|
||||||
|
&&L_OP_EQI,
|
||||||
|
&&L_OP_LTI,
|
||||||
|
&&L_OP_LEI,
|
||||||
|
&&L_OP_GTI,
|
||||||
|
&&L_OP_GEI,
|
||||||
|
&&L_OP_TEST,
|
||||||
|
&&L_OP_TESTSET,
|
||||||
|
&&L_OP_CALL,
|
||||||
|
&&L_OP_TAILCALL,
|
||||||
|
&&L_OP_RETURN,
|
||||||
|
&&L_OP_RETURN0,
|
||||||
|
&&L_OP_RETURN1,
|
||||||
|
&&L_OP_FORLOOP,
|
||||||
|
&&L_OP_FORPREP,
|
||||||
|
&&L_OP_TFORPREP,
|
||||||
|
&&L_OP_TFORCALL,
|
||||||
|
&&L_OP_TFORLOOP,
|
||||||
|
&&L_OP_SETLIST,
|
||||||
|
&&L_OP_CLOSURE,
|
||||||
|
&&L_OP_VARARG,
|
||||||
|
&&L_OP_VARARGPREP,
|
||||||
|
&&L_OP_EXTRAARG
|
||||||
|
|
||||||
|
};
|
||||||
@@ -0,0 +1,581 @@
|
|||||||
|
/*
|
||||||
|
** $Id: llex.c $
|
||||||
|
** Lexical Analyzer
|
||||||
|
** See Copyright Notice in lua.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define llex_c
|
||||||
|
#define LUA_CORE
|
||||||
|
|
||||||
|
#include "lprefix.h"
|
||||||
|
|
||||||
|
|
||||||
|
#include <locale.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "lua.h"
|
||||||
|
|
||||||
|
#include "lctype.h"
|
||||||
|
#include "ldebug.h"
|
||||||
|
#include "ldo.h"
|
||||||
|
#include "lgc.h"
|
||||||
|
#include "llex.h"
|
||||||
|
#include "lobject.h"
|
||||||
|
#include "lparser.h"
|
||||||
|
#include "lstate.h"
|
||||||
|
#include "lstring.h"
|
||||||
|
#include "ltable.h"
|
||||||
|
#include "lzio.h"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#define next(ls) (ls->current = zgetc(ls->z))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#define currIsNewline(ls) (ls->current == '\n' || ls->current == '\r')
|
||||||
|
|
||||||
|
|
||||||
|
/* ORDER RESERVED */
|
||||||
|
static const char *const luaX_tokens [] = {
|
||||||
|
"and", "break", "do", "else", "elseif",
|
||||||
|
"end", "false", "for", "function", "goto", "if",
|
||||||
|
"in", "local", "nil", "not", "or", "repeat",
|
||||||
|
"return", "then", "true", "until", "while",
|
||||||
|
"//", "..", "...", "==", ">=", "<=", "~=",
|
||||||
|
"<<", ">>", "::", "<eof>",
|
||||||
|
"<number>", "<integer>", "<name>", "<string>"
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#define save_and_next(ls) (save(ls, ls->current), next(ls))
|
||||||
|
|
||||||
|
|
||||||
|
static l_noret lexerror (LexState *ls, const char *msg, int token);
|
||||||
|
|
||||||
|
|
||||||
|
static void save (LexState *ls, int c) {
|
||||||
|
Mbuffer *b = ls->buff;
|
||||||
|
if (luaZ_bufflen(b) + 1 > luaZ_sizebuffer(b)) {
|
||||||
|
size_t newsize;
|
||||||
|
if (luaZ_sizebuffer(b) >= MAX_SIZE/2)
|
||||||
|
lexerror(ls, "lexical element too long", 0);
|
||||||
|
newsize = luaZ_sizebuffer(b) * 2;
|
||||||
|
luaZ_resizebuffer(ls->L, b, newsize);
|
||||||
|
}
|
||||||
|
b->buffer[luaZ_bufflen(b)++] = cast_char(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void luaX_init (lua_State *L) {
|
||||||
|
int i;
|
||||||
|
TString *e = luaS_newliteral(L, LUA_ENV); /* create env name */
|
||||||
|
luaC_fix(L, obj2gco(e)); /* never collect this name */
|
||||||
|
for (i=0; i<NUM_RESERVED; i++) {
|
||||||
|
TString *ts = luaS_new(L, luaX_tokens[i]);
|
||||||
|
luaC_fix(L, obj2gco(ts)); /* reserved words are never collected */
|
||||||
|
ts->extra = cast_byte(i+1); /* reserved word */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const char *luaX_token2str (LexState *ls, int token) {
|
||||||
|
if (token < FIRST_RESERVED) { /* single-byte symbols? */
|
||||||
|
if (lisprint(token))
|
||||||
|
return luaO_pushfstring(ls->L, "'%c'", token);
|
||||||
|
else /* control character */
|
||||||
|
return luaO_pushfstring(ls->L, "'<\\%d>'", token);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const char *s = luaX_tokens[token - FIRST_RESERVED];
|
||||||
|
if (token < TK_EOS) /* fixed format (symbols and reserved words)? */
|
||||||
|
return luaO_pushfstring(ls->L, "'%s'", s);
|
||||||
|
else /* names, strings, and numerals */
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static const char *txtToken (LexState *ls, int token) {
|
||||||
|
switch (token) {
|
||||||
|
case TK_NAME: case TK_STRING:
|
||||||
|
case TK_FLT: case TK_INT:
|
||||||
|
save(ls, '\0');
|
||||||
|
return luaO_pushfstring(ls->L, "'%s'", luaZ_buffer(ls->buff));
|
||||||
|
default:
|
||||||
|
return luaX_token2str(ls, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static l_noret lexerror (LexState *ls, const char *msg, int token) {
|
||||||
|
msg = luaG_addinfo(ls->L, msg, ls->source, ls->linenumber);
|
||||||
|
if (token)
|
||||||
|
luaO_pushfstring(ls->L, "%s near %s", msg, txtToken(ls, token));
|
||||||
|
luaD_throw(ls->L, LUA_ERRSYNTAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
l_noret luaX_syntaxerror (LexState *ls, const char *msg) {
|
||||||
|
lexerror(ls, msg, ls->t.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Creates a new string and anchors it in scanner's table so that it
|
||||||
|
** will not be collected until the end of the compilation; by that time
|
||||||
|
** it should be anchored somewhere. It also internalizes long strings,
|
||||||
|
** ensuring there is only one copy of each unique string. The table
|
||||||
|
** here is used as a set: the string enters as the key, while its value
|
||||||
|
** is irrelevant. We use the string itself as the value only because it
|
||||||
|
** is a TValue readily available. Later, the code generation can change
|
||||||
|
** this value.
|
||||||
|
*/
|
||||||
|
TString *luaX_newstring (LexState *ls, const char *str, size_t l) {
|
||||||
|
lua_State *L = ls->L;
|
||||||
|
TString *ts = luaS_newlstr(L, str, l); /* create new string */
|
||||||
|
const TValue *o = luaH_getstr(ls->h, ts);
|
||||||
|
if (!ttisnil(o)) /* string already present? */
|
||||||
|
ts = keystrval(nodefromval(o)); /* get saved copy */
|
||||||
|
else { /* not in use yet */
|
||||||
|
TValue *stv = s2v(L->top.p++); /* reserve stack space for string */
|
||||||
|
setsvalue(L, stv, ts); /* temporarily anchor the string */
|
||||||
|
luaH_finishset(L, ls->h, stv, o, stv); /* t[string] = string */
|
||||||
|
/* table is not a metatable, so it does not need to invalidate cache */
|
||||||
|
luaC_checkGC(L);
|
||||||
|
L->top.p--; /* remove string from stack */
|
||||||
|
}
|
||||||
|
return ts;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** increment line number and skips newline sequence (any of
|
||||||
|
** \n, \r, \n\r, or \r\n)
|
||||||
|
*/
|
||||||
|
static void inclinenumber (LexState *ls) {
|
||||||
|
int old = ls->current;
|
||||||
|
lua_assert(currIsNewline(ls));
|
||||||
|
next(ls); /* skip '\n' or '\r' */
|
||||||
|
if (currIsNewline(ls) && ls->current != old)
|
||||||
|
next(ls); /* skip '\n\r' or '\r\n' */
|
||||||
|
if (++ls->linenumber >= MAX_INT)
|
||||||
|
lexerror(ls, "chunk has too many lines", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void luaX_setinput (lua_State *L, LexState *ls, ZIO *z, TString *source,
|
||||||
|
int firstchar) {
|
||||||
|
ls->t.token = 0;
|
||||||
|
ls->L = L;
|
||||||
|
ls->current = firstchar;
|
||||||
|
ls->lookahead.token = TK_EOS; /* no look-ahead token */
|
||||||
|
ls->z = z;
|
||||||
|
ls->fs = NULL;
|
||||||
|
ls->linenumber = 1;
|
||||||
|
ls->lastline = 1;
|
||||||
|
ls->source = source;
|
||||||
|
ls->envn = luaS_newliteral(L, LUA_ENV); /* get env name */
|
||||||
|
luaZ_resizebuffer(ls->L, ls->buff, LUA_MINBUFFER); /* initialize buffer */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** =======================================================
|
||||||
|
** LEXICAL ANALYZER
|
||||||
|
** =======================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
static int check_next1 (LexState *ls, int c) {
|
||||||
|
if (ls->current == c) {
|
||||||
|
next(ls);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
else return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Check whether current char is in set 'set' (with two chars) and
|
||||||
|
** saves it
|
||||||
|
*/
|
||||||
|
static int check_next2 (LexState *ls, const char *set) {
|
||||||
|
lua_assert(set[2] == '\0');
|
||||||
|
if (ls->current == set[0] || ls->current == set[1]) {
|
||||||
|
save_and_next(ls);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
else return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* LUA_NUMBER */
|
||||||
|
/*
|
||||||
|
** This function is quite liberal in what it accepts, as 'luaO_str2num'
|
||||||
|
** will reject ill-formed numerals. Roughly, it accepts the following
|
||||||
|
** pattern:
|
||||||
|
**
|
||||||
|
** %d(%x|%.|([Ee][+-]?))* | 0[Xx](%x|%.|([Pp][+-]?))*
|
||||||
|
**
|
||||||
|
** The only tricky part is to accept [+-] only after a valid exponent
|
||||||
|
** mark, to avoid reading '3-4' or '0xe+1' as a single number.
|
||||||
|
**
|
||||||
|
** The caller might have already read an initial dot.
|
||||||
|
*/
|
||||||
|
static int read_numeral (LexState *ls, SemInfo *seminfo) {
|
||||||
|
TValue obj;
|
||||||
|
const char *expo = "Ee";
|
||||||
|
int first = ls->current;
|
||||||
|
lua_assert(lisdigit(ls->current));
|
||||||
|
save_and_next(ls);
|
||||||
|
if (first == '0' && check_next2(ls, "xX")) /* hexadecimal? */
|
||||||
|
expo = "Pp";
|
||||||
|
for (;;) {
|
||||||
|
if (check_next2(ls, expo)) /* exponent mark? */
|
||||||
|
check_next2(ls, "-+"); /* optional exponent sign */
|
||||||
|
else if (lisxdigit(ls->current) || ls->current == '.') /* '%x|%.' */
|
||||||
|
save_and_next(ls);
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
if (lislalpha(ls->current)) /* is numeral touching a letter? */
|
||||||
|
save_and_next(ls); /* force an error */
|
||||||
|
save(ls, '\0');
|
||||||
|
if (luaO_str2num(luaZ_buffer(ls->buff), &obj) == 0) /* format error? */
|
||||||
|
lexerror(ls, "malformed number", TK_FLT);
|
||||||
|
if (ttisinteger(&obj)) {
|
||||||
|
seminfo->i = ivalue(&obj);
|
||||||
|
return TK_INT;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
lua_assert(ttisfloat(&obj));
|
||||||
|
seminfo->r = fltvalue(&obj);
|
||||||
|
return TK_FLT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** read a sequence '[=*[' or ']=*]', leaving the last bracket. If
|
||||||
|
** sequence is well formed, return its number of '='s + 2; otherwise,
|
||||||
|
** return 1 if it is a single bracket (no '='s and no 2nd bracket);
|
||||||
|
** otherwise (an unfinished '[==...') return 0.
|
||||||
|
*/
|
||||||
|
static size_t skip_sep (LexState *ls) {
|
||||||
|
size_t count = 0;
|
||||||
|
int s = ls->current;
|
||||||
|
lua_assert(s == '[' || s == ']');
|
||||||
|
save_and_next(ls);
|
||||||
|
while (ls->current == '=') {
|
||||||
|
save_and_next(ls);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return (ls->current == s) ? count + 2
|
||||||
|
: (count == 0) ? 1
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void read_long_string (LexState *ls, SemInfo *seminfo, size_t sep) {
|
||||||
|
int line = ls->linenumber; /* initial line (for error message) */
|
||||||
|
save_and_next(ls); /* skip 2nd '[' */
|
||||||
|
if (currIsNewline(ls)) /* string starts with a newline? */
|
||||||
|
inclinenumber(ls); /* skip it */
|
||||||
|
for (;;) {
|
||||||
|
switch (ls->current) {
|
||||||
|
case EOZ: { /* error */
|
||||||
|
const char *what = (seminfo ? "string" : "comment");
|
||||||
|
const char *msg = luaO_pushfstring(ls->L,
|
||||||
|
"unfinished long %s (starting at line %d)", what, line);
|
||||||
|
lexerror(ls, msg, TK_EOS);
|
||||||
|
break; /* to avoid warnings */
|
||||||
|
}
|
||||||
|
case ']': {
|
||||||
|
if (skip_sep(ls) == sep) {
|
||||||
|
save_and_next(ls); /* skip 2nd ']' */
|
||||||
|
goto endloop;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case '\n': case '\r': {
|
||||||
|
save(ls, '\n');
|
||||||
|
inclinenumber(ls);
|
||||||
|
if (!seminfo) luaZ_resetbuffer(ls->buff); /* avoid wasting space */
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
if (seminfo) save_and_next(ls);
|
||||||
|
else next(ls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} endloop:
|
||||||
|
if (seminfo)
|
||||||
|
seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff) + sep,
|
||||||
|
luaZ_bufflen(ls->buff) - 2 * sep);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void esccheck (LexState *ls, int c, const char *msg) {
|
||||||
|
if (!c) {
|
||||||
|
if (ls->current != EOZ)
|
||||||
|
save_and_next(ls); /* add current to buffer for error message */
|
||||||
|
lexerror(ls, msg, TK_STRING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int gethexa (LexState *ls) {
|
||||||
|
save_and_next(ls);
|
||||||
|
esccheck (ls, lisxdigit(ls->current), "hexadecimal digit expected");
|
||||||
|
return luaO_hexavalue(ls->current);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int readhexaesc (LexState *ls) {
|
||||||
|
int r = gethexa(ls);
|
||||||
|
r = (r << 4) + gethexa(ls);
|
||||||
|
luaZ_buffremove(ls->buff, 2); /* remove saved chars from buffer */
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static unsigned long readutf8esc (LexState *ls) {
|
||||||
|
unsigned long r;
|
||||||
|
int i = 4; /* chars to be removed: '\', 'u', '{', and first digit */
|
||||||
|
save_and_next(ls); /* skip 'u' */
|
||||||
|
esccheck(ls, ls->current == '{', "missing '{'");
|
||||||
|
r = gethexa(ls); /* must have at least one digit */
|
||||||
|
while (cast_void(save_and_next(ls)), lisxdigit(ls->current)) {
|
||||||
|
i++;
|
||||||
|
esccheck(ls, r <= (0x7FFFFFFFu >> 4), "UTF-8 value too large");
|
||||||
|
r = (r << 4) + luaO_hexavalue(ls->current);
|
||||||
|
}
|
||||||
|
esccheck(ls, ls->current == '}', "missing '}'");
|
||||||
|
next(ls); /* skip '}' */
|
||||||
|
luaZ_buffremove(ls->buff, i); /* remove saved chars from buffer */
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void utf8esc (LexState *ls) {
|
||||||
|
char buff[UTF8BUFFSZ];
|
||||||
|
int n = luaO_utf8esc(buff, readutf8esc(ls));
|
||||||
|
for (; n > 0; n--) /* add 'buff' to string */
|
||||||
|
save(ls, buff[UTF8BUFFSZ - n]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int readdecesc (LexState *ls) {
|
||||||
|
int i;
|
||||||
|
int r = 0; /* result accumulator */
|
||||||
|
for (i = 0; i < 3 && lisdigit(ls->current); i++) { /* read up to 3 digits */
|
||||||
|
r = 10*r + ls->current - '0';
|
||||||
|
save_and_next(ls);
|
||||||
|
}
|
||||||
|
esccheck(ls, r <= UCHAR_MAX, "decimal escape too large");
|
||||||
|
luaZ_buffremove(ls->buff, i); /* remove read digits from buffer */
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void read_string (LexState *ls, int del, SemInfo *seminfo) {
|
||||||
|
save_and_next(ls); /* keep delimiter (for error messages) */
|
||||||
|
while (ls->current != del) {
|
||||||
|
switch (ls->current) {
|
||||||
|
case EOZ:
|
||||||
|
lexerror(ls, "unfinished string", TK_EOS);
|
||||||
|
break; /* to avoid warnings */
|
||||||
|
case '\n':
|
||||||
|
case '\r':
|
||||||
|
lexerror(ls, "unfinished string", TK_STRING);
|
||||||
|
break; /* to avoid warnings */
|
||||||
|
case '\\': { /* escape sequences */
|
||||||
|
int c; /* final character to be saved */
|
||||||
|
save_and_next(ls); /* keep '\\' for error messages */
|
||||||
|
switch (ls->current) {
|
||||||
|
case 'a': c = '\a'; goto read_save;
|
||||||
|
case 'b': c = '\b'; goto read_save;
|
||||||
|
case 'f': c = '\f'; goto read_save;
|
||||||
|
case 'n': c = '\n'; goto read_save;
|
||||||
|
case 'r': c = '\r'; goto read_save;
|
||||||
|
case 't': c = '\t'; goto read_save;
|
||||||
|
case 'v': c = '\v'; goto read_save;
|
||||||
|
case 'x': c = readhexaesc(ls); goto read_save;
|
||||||
|
case 'u': utf8esc(ls); goto no_save;
|
||||||
|
case '\n': case '\r':
|
||||||
|
inclinenumber(ls); c = '\n'; goto only_save;
|
||||||
|
case '\\': case '\"': case '\'':
|
||||||
|
c = ls->current; goto read_save;
|
||||||
|
case EOZ: goto no_save; /* will raise an error next loop */
|
||||||
|
case 'z': { /* zap following span of spaces */
|
||||||
|
luaZ_buffremove(ls->buff, 1); /* remove '\\' */
|
||||||
|
next(ls); /* skip the 'z' */
|
||||||
|
while (lisspace(ls->current)) {
|
||||||
|
if (currIsNewline(ls)) inclinenumber(ls);
|
||||||
|
else next(ls);
|
||||||
|
}
|
||||||
|
goto no_save;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
esccheck(ls, lisdigit(ls->current), "invalid escape sequence");
|
||||||
|
c = readdecesc(ls); /* digital escape '\ddd' */
|
||||||
|
goto only_save;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read_save:
|
||||||
|
next(ls);
|
||||||
|
/* go through */
|
||||||
|
only_save:
|
||||||
|
luaZ_buffremove(ls->buff, 1); /* remove '\\' */
|
||||||
|
save(ls, c);
|
||||||
|
/* go through */
|
||||||
|
no_save: break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
save_and_next(ls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
save_and_next(ls); /* skip delimiter */
|
||||||
|
seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff) + 1,
|
||||||
|
luaZ_bufflen(ls->buff) - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int llex (LexState *ls, SemInfo *seminfo) {
|
||||||
|
luaZ_resetbuffer(ls->buff);
|
||||||
|
for (;;) {
|
||||||
|
switch (ls->current) {
|
||||||
|
case '\n': case '\r': { /* line breaks */
|
||||||
|
inclinenumber(ls);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ' ': case '\f': case '\t': case '\v': { /* spaces */
|
||||||
|
next(ls);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case '-': { /* '-' or '--' (comment) */
|
||||||
|
next(ls);
|
||||||
|
if (ls->current != '-') return '-';
|
||||||
|
/* else is a comment */
|
||||||
|
next(ls);
|
||||||
|
if (ls->current == '[') { /* long comment? */
|
||||||
|
size_t sep = skip_sep(ls);
|
||||||
|
luaZ_resetbuffer(ls->buff); /* 'skip_sep' may dirty the buffer */
|
||||||
|
if (sep >= 2) {
|
||||||
|
read_long_string(ls, NULL, sep); /* skip long comment */
|
||||||
|
luaZ_resetbuffer(ls->buff); /* previous call may dirty the buff. */
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* else short comment */
|
||||||
|
while (!currIsNewline(ls) && ls->current != EOZ)
|
||||||
|
next(ls); /* skip until end of line (or end of file) */
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case '[': { /* long string or simply '[' */
|
||||||
|
size_t sep = skip_sep(ls);
|
||||||
|
if (sep >= 2) {
|
||||||
|
read_long_string(ls, seminfo, sep);
|
||||||
|
return TK_STRING;
|
||||||
|
}
|
||||||
|
else if (sep == 0) /* '[=...' missing second bracket? */
|
||||||
|
lexerror(ls, "invalid long string delimiter", TK_STRING);
|
||||||
|
return '[';
|
||||||
|
}
|
||||||
|
case '=': {
|
||||||
|
next(ls);
|
||||||
|
if (check_next1(ls, '=')) return TK_EQ; /* '==' */
|
||||||
|
else return '=';
|
||||||
|
}
|
||||||
|
case '<': {
|
||||||
|
next(ls);
|
||||||
|
if (check_next1(ls, '=')) return TK_LE; /* '<=' */
|
||||||
|
else if (check_next1(ls, '<')) return TK_SHL; /* '<<' */
|
||||||
|
else return '<';
|
||||||
|
}
|
||||||
|
case '>': {
|
||||||
|
next(ls);
|
||||||
|
if (check_next1(ls, '=')) return TK_GE; /* '>=' */
|
||||||
|
else if (check_next1(ls, '>')) return TK_SHR; /* '>>' */
|
||||||
|
else return '>';
|
||||||
|
}
|
||||||
|
case '/': {
|
||||||
|
next(ls);
|
||||||
|
if (check_next1(ls, '/')) return TK_IDIV; /* '//' */
|
||||||
|
else return '/';
|
||||||
|
}
|
||||||
|
case '~': {
|
||||||
|
next(ls);
|
||||||
|
if (check_next1(ls, '=')) return TK_NE; /* '~=' */
|
||||||
|
else return '~';
|
||||||
|
}
|
||||||
|
case ':': {
|
||||||
|
next(ls);
|
||||||
|
if (check_next1(ls, ':')) return TK_DBCOLON; /* '::' */
|
||||||
|
else return ':';
|
||||||
|
}
|
||||||
|
case '"': case '\'': { /* short literal strings */
|
||||||
|
read_string(ls, ls->current, seminfo);
|
||||||
|
return TK_STRING;
|
||||||
|
}
|
||||||
|
case '.': { /* '.', '..', '...', or number */
|
||||||
|
save_and_next(ls);
|
||||||
|
if (check_next1(ls, '.')) {
|
||||||
|
if (check_next1(ls, '.'))
|
||||||
|
return TK_DOTS; /* '...' */
|
||||||
|
else return TK_CONCAT; /* '..' */
|
||||||
|
}
|
||||||
|
else if (!lisdigit(ls->current)) return '.';
|
||||||
|
else return read_numeral(ls, seminfo);
|
||||||
|
}
|
||||||
|
case '0': case '1': case '2': case '3': case '4':
|
||||||
|
case '5': case '6': case '7': case '8': case '9': {
|
||||||
|
return read_numeral(ls, seminfo);
|
||||||
|
}
|
||||||
|
case EOZ: {
|
||||||
|
return TK_EOS;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
if (lislalpha(ls->current)) { /* identifier or reserved word? */
|
||||||
|
TString *ts;
|
||||||
|
do {
|
||||||
|
save_and_next(ls);
|
||||||
|
} while (lislalnum(ls->current));
|
||||||
|
ts = luaX_newstring(ls, luaZ_buffer(ls->buff),
|
||||||
|
luaZ_bufflen(ls->buff));
|
||||||
|
seminfo->ts = ts;
|
||||||
|
if (isreserved(ts)) /* reserved word? */
|
||||||
|
return ts->extra - 1 + FIRST_RESERVED;
|
||||||
|
else {
|
||||||
|
return TK_NAME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else { /* single-char tokens ('+', '*', '%', '{', '}', ...) */
|
||||||
|
int c = ls->current;
|
||||||
|
next(ls);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void luaX_next (LexState *ls) {
|
||||||
|
ls->lastline = ls->linenumber;
|
||||||
|
if (ls->lookahead.token != TK_EOS) { /* is there a look-ahead token? */
|
||||||
|
ls->t = ls->lookahead; /* use this one */
|
||||||
|
ls->lookahead.token = TK_EOS; /* and discharge it */
|
||||||
|
}
|
||||||
|
else
|
||||||
|
ls->t.token = llex(ls, &ls->t.seminfo); /* read next token */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int luaX_lookahead (LexState *ls) {
|
||||||
|
lua_assert(ls->lookahead.token == TK_EOS);
|
||||||
|
ls->lookahead.token = llex(ls, &ls->lookahead.seminfo);
|
||||||
|
return ls->lookahead.token;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
** $Id: llex.h $
|
||||||
|
** Lexical Analyzer
|
||||||
|
** See Copyright Notice in lua.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef llex_h
|
||||||
|
#define llex_h
|
||||||
|
|
||||||
|
#include <limits.h>
|
||||||
|
|
||||||
|
#include "lobject.h"
|
||||||
|
#include "lzio.h"
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Single-char tokens (terminal symbols) are represented by their own
|
||||||
|
** numeric code. Other tokens start at the following value.
|
||||||
|
*/
|
||||||
|
#define FIRST_RESERVED (UCHAR_MAX + 1)
|
||||||
|
|
||||||
|
|
||||||
|
#if !defined(LUA_ENV)
|
||||||
|
#define LUA_ENV "_ENV"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* WARNING: if you change the order of this enumeration,
|
||||||
|
* grep "ORDER RESERVED"
|
||||||
|
*/
|
||||||
|
enum RESERVED {
|
||||||
|
/* terminal symbols denoted by reserved words */
|
||||||
|
TK_AND = FIRST_RESERVED, TK_BREAK,
|
||||||
|
TK_DO, TK_ELSE, TK_ELSEIF, TK_END, TK_FALSE, TK_FOR, TK_FUNCTION,
|
||||||
|
TK_GOTO, TK_IF, TK_IN, TK_LOCAL, TK_NIL, TK_NOT, TK_OR, TK_REPEAT,
|
||||||
|
TK_RETURN, TK_THEN, TK_TRUE, TK_UNTIL, TK_WHILE,
|
||||||
|
/* other terminal symbols */
|
||||||
|
TK_IDIV, TK_CONCAT, TK_DOTS, TK_EQ, TK_GE, TK_LE, TK_NE,
|
||||||
|
TK_SHL, TK_SHR,
|
||||||
|
TK_DBCOLON, TK_EOS,
|
||||||
|
TK_FLT, TK_INT, TK_NAME, TK_STRING
|
||||||
|
};
|
||||||
|
|
||||||
|
/* number of reserved words */
|
||||||
|
#define NUM_RESERVED (cast_int(TK_WHILE-FIRST_RESERVED + 1))
|
||||||
|
|
||||||
|
|
||||||
|
typedef union {
|
||||||
|
lua_Number r;
|
||||||
|
lua_Integer i;
|
||||||
|
TString *ts;
|
||||||
|
} SemInfo; /* semantics information */
|
||||||
|
|
||||||
|
|
||||||
|
typedef struct Token {
|
||||||
|
int token;
|
||||||
|
SemInfo seminfo;
|
||||||
|
} Token;
|
||||||
|
|
||||||
|
|
||||||
|
/* state of the lexer plus state of the parser when shared by all
|
||||||
|
functions */
|
||||||
|
typedef struct LexState {
|
||||||
|
int current; /* current character (charint) */
|
||||||
|
int linenumber; /* input line counter */
|
||||||
|
int lastline; /* line of last token 'consumed' */
|
||||||
|
Token t; /* current token */
|
||||||
|
Token lookahead; /* look ahead token */
|
||||||
|
struct FuncState *fs; /* current function (parser) */
|
||||||
|
struct lua_State *L;
|
||||||
|
ZIO *z; /* input stream */
|
||||||
|
Mbuffer *buff; /* buffer for tokens */
|
||||||
|
Table *h; /* to avoid collection/reuse strings */
|
||||||
|
struct Dyndata *dyd; /* dynamic structures used by the parser */
|
||||||
|
TString *source; /* current source name */
|
||||||
|
TString *envn; /* environment variable name */
|
||||||
|
} LexState;
|
||||||
|
|
||||||
|
|
||||||
|
LUAI_FUNC void luaX_init (lua_State *L);
|
||||||
|
LUAI_FUNC void luaX_setinput (lua_State *L, LexState *ls, ZIO *z,
|
||||||
|
TString *source, int firstchar);
|
||||||
|
LUAI_FUNC TString *luaX_newstring (LexState *ls, const char *str, size_t l);
|
||||||
|
LUAI_FUNC void luaX_next (LexState *ls);
|
||||||
|
LUAI_FUNC int luaX_lookahead (LexState *ls);
|
||||||
|
LUAI_FUNC l_noret luaX_syntaxerror (LexState *ls, const char *s);
|
||||||
|
LUAI_FUNC const char *luaX_token2str (LexState *ls, int token);
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,380 @@
|
|||||||
|
/*
|
||||||
|
** $Id: llimits.h $
|
||||||
|
** Limits, basic types, and some other 'installation-dependent' definitions
|
||||||
|
** See Copyright Notice in lua.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef llimits_h
|
||||||
|
#define llimits_h
|
||||||
|
|
||||||
|
|
||||||
|
#include <limits.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
|
||||||
|
#include "lua.h"
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** 'lu_mem' and 'l_mem' are unsigned/signed integers big enough to count
|
||||||
|
** the total memory used by Lua (in bytes). Usually, 'size_t' and
|
||||||
|
** 'ptrdiff_t' should work, but we use 'long' for 16-bit machines.
|
||||||
|
*/
|
||||||
|
#if defined(LUAI_MEM) /* { external definitions? */
|
||||||
|
typedef LUAI_UMEM lu_mem;
|
||||||
|
typedef LUAI_MEM l_mem;
|
||||||
|
#elif LUAI_IS32INT /* }{ */
|
||||||
|
typedef size_t lu_mem;
|
||||||
|
typedef ptrdiff_t l_mem;
|
||||||
|
#else /* 16-bit ints */ /* }{ */
|
||||||
|
typedef unsigned long lu_mem;
|
||||||
|
typedef long l_mem;
|
||||||
|
#endif /* } */
|
||||||
|
|
||||||
|
|
||||||
|
/* chars used as small naturals (so that 'char' is reserved for characters) */
|
||||||
|
typedef unsigned char lu_byte;
|
||||||
|
typedef signed char ls_byte;
|
||||||
|
|
||||||
|
|
||||||
|
/* maximum value for size_t */
|
||||||
|
#define MAX_SIZET ((size_t)(~(size_t)0))
|
||||||
|
|
||||||
|
/* maximum size visible for Lua (must be representable in a lua_Integer) */
|
||||||
|
#define MAX_SIZE (sizeof(size_t) < sizeof(lua_Integer) ? MAX_SIZET \
|
||||||
|
: (size_t)(LUA_MAXINTEGER))
|
||||||
|
|
||||||
|
|
||||||
|
#define MAX_LUMEM ((lu_mem)(~(lu_mem)0))
|
||||||
|
|
||||||
|
#define MAX_LMEM ((l_mem)(MAX_LUMEM >> 1))
|
||||||
|
|
||||||
|
|
||||||
|
#define MAX_INT INT_MAX /* maximum value of an int */
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** floor of the log2 of the maximum signed value for integral type 't'.
|
||||||
|
** (That is, maximum 'n' such that '2^n' fits in the given signed type.)
|
||||||
|
*/
|
||||||
|
#define log2maxs(t) (sizeof(t) * 8 - 2)
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** test whether an unsigned value is a power of 2 (or zero)
|
||||||
|
*/
|
||||||
|
#define ispow2(x) (((x) & ((x) - 1)) == 0)
|
||||||
|
|
||||||
|
|
||||||
|
/* number of chars of a literal string without the ending \0 */
|
||||||
|
#define LL(x) (sizeof(x)/sizeof(char) - 1)
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** conversion of pointer to unsigned integer: this is for hashing only;
|
||||||
|
** there is no problem if the integer cannot hold the whole pointer
|
||||||
|
** value. (In strict ISO C this may cause undefined behavior, but no
|
||||||
|
** actual machine seems to bother.)
|
||||||
|
*/
|
||||||
|
#if !defined(LUA_USE_C89) && defined(__STDC_VERSION__) && \
|
||||||
|
__STDC_VERSION__ >= 199901L
|
||||||
|
#include <stdint.h>
|
||||||
|
#if defined(UINTPTR_MAX) /* even in C99 this type is optional */
|
||||||
|
#define L_P2I uintptr_t
|
||||||
|
#else /* no 'intptr'? */
|
||||||
|
#define L_P2I uintmax_t /* use the largest available integer */
|
||||||
|
#endif
|
||||||
|
#else /* C89 option */
|
||||||
|
#define L_P2I size_t
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define point2uint(p) ((unsigned int)((L_P2I)(p) & UINT_MAX))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* types of 'usual argument conversions' for lua_Number and lua_Integer */
|
||||||
|
typedef LUAI_UACNUMBER l_uacNumber;
|
||||||
|
typedef LUAI_UACINT l_uacInt;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Internal assertions for in-house debugging
|
||||||
|
*/
|
||||||
|
#if defined LUAI_ASSERT
|
||||||
|
#undef NDEBUG
|
||||||
|
#include <assert.h>
|
||||||
|
#define lua_assert(c) assert(c)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(lua_assert)
|
||||||
|
#define check_exp(c,e) (lua_assert(c), (e))
|
||||||
|
/* to avoid problems with conditions too long */
|
||||||
|
#define lua_longassert(c) ((c) ? (void)0 : lua_assert(0))
|
||||||
|
#else
|
||||||
|
#define lua_assert(c) ((void)0)
|
||||||
|
#define check_exp(c,e) (e)
|
||||||
|
#define lua_longassert(c) ((void)0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
** assertion for checking API calls
|
||||||
|
*/
|
||||||
|
#if !defined(luai_apicheck)
|
||||||
|
#define luai_apicheck(l,e) ((void)l, lua_assert(e))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define api_check(l,e,msg) luai_apicheck(l,(e) && msg)
|
||||||
|
|
||||||
|
|
||||||
|
/* macro to avoid warnings about unused variables */
|
||||||
|
#if !defined(UNUSED)
|
||||||
|
#define UNUSED(x) ((void)(x))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/* type casts (a macro highlights casts in the code) */
|
||||||
|
#define cast(t, exp) ((t)(exp))
|
||||||
|
|
||||||
|
#define cast_void(i) cast(void, (i))
|
||||||
|
#define cast_voidp(i) cast(void *, (i))
|
||||||
|
#define cast_num(i) cast(lua_Number, (i))
|
||||||
|
#define cast_int(i) cast(int, (i))
|
||||||
|
#define cast_uint(i) cast(unsigned int, (i))
|
||||||
|
#define cast_byte(i) cast(lu_byte, (i))
|
||||||
|
#define cast_uchar(i) cast(unsigned char, (i))
|
||||||
|
#define cast_char(i) cast(char, (i))
|
||||||
|
#define cast_charp(i) cast(char *, (i))
|
||||||
|
#define cast_sizet(i) cast(size_t, (i))
|
||||||
|
|
||||||
|
|
||||||
|
/* cast a signed lua_Integer to lua_Unsigned */
|
||||||
|
#if !defined(l_castS2U)
|
||||||
|
#define l_castS2U(i) ((lua_Unsigned)(i))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
** cast a lua_Unsigned to a signed lua_Integer; this cast is
|
||||||
|
** not strict ISO C, but two-complement architectures should
|
||||||
|
** work fine.
|
||||||
|
*/
|
||||||
|
#if !defined(l_castU2S)
|
||||||
|
#define l_castU2S(i) ((lua_Integer)(i))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** non-return type
|
||||||
|
*/
|
||||||
|
#if !defined(l_noret)
|
||||||
|
|
||||||
|
#if defined(__GNUC__)
|
||||||
|
#define l_noret void __attribute__((noreturn))
|
||||||
|
#elif defined(_MSC_VER) && _MSC_VER >= 1200
|
||||||
|
#define l_noret void __declspec(noreturn)
|
||||||
|
#else
|
||||||
|
#define l_noret void
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Inline functions
|
||||||
|
*/
|
||||||
|
#if !defined(LUA_USE_C89)
|
||||||
|
#define l_inline inline
|
||||||
|
#elif defined(__GNUC__)
|
||||||
|
#define l_inline __inline__
|
||||||
|
#else
|
||||||
|
#define l_inline /* empty */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define l_sinline static l_inline
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** type for virtual-machine instructions;
|
||||||
|
** must be an unsigned with (at least) 4 bytes (see details in lopcodes.h)
|
||||||
|
*/
|
||||||
|
#if LUAI_IS32INT
|
||||||
|
typedef unsigned int l_uint32;
|
||||||
|
#else
|
||||||
|
typedef unsigned long l_uint32;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef l_uint32 Instruction;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Maximum length for short strings, that is, strings that are
|
||||||
|
** internalized. (Cannot be smaller than reserved words or tags for
|
||||||
|
** metamethods, as these strings must be internalized;
|
||||||
|
** #("function") = 8, #("__newindex") = 10.)
|
||||||
|
*/
|
||||||
|
#if !defined(LUAI_MAXSHORTLEN)
|
||||||
|
#define LUAI_MAXSHORTLEN 40
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Initial size for the string table (must be power of 2).
|
||||||
|
** The Lua core alone registers ~50 strings (reserved words +
|
||||||
|
** metaevent keys + a few others). Libraries would typically add
|
||||||
|
** a few dozens more.
|
||||||
|
*/
|
||||||
|
#if !defined(MINSTRTABSIZE)
|
||||||
|
#define MINSTRTABSIZE 128
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Size of cache for strings in the API. 'N' is the number of
|
||||||
|
** sets (better be a prime) and "M" is the size of each set (M == 1
|
||||||
|
** makes a direct cache.)
|
||||||
|
*/
|
||||||
|
#if !defined(STRCACHE_N)
|
||||||
|
#define STRCACHE_N 53
|
||||||
|
#define STRCACHE_M 2
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/* minimum size for string buffer */
|
||||||
|
#if !defined(LUA_MINBUFFER)
|
||||||
|
#define LUA_MINBUFFER 32
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Maximum depth for nested C calls, syntactical nested non-terminals,
|
||||||
|
** and other features implemented through recursion in C. (Value must
|
||||||
|
** fit in a 16-bit unsigned integer. It must also be compatible with
|
||||||
|
** the size of the C stack.)
|
||||||
|
*/
|
||||||
|
#if !defined(LUAI_MAXCCALLS)
|
||||||
|
#define LUAI_MAXCCALLS 200
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** macros that are executed whenever program enters the Lua core
|
||||||
|
** ('lua_lock') and leaves the core ('lua_unlock')
|
||||||
|
*/
|
||||||
|
#if !defined(lua_lock)
|
||||||
|
#define lua_lock(L) ((void) 0)
|
||||||
|
#define lua_unlock(L) ((void) 0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
** macro executed during Lua functions at points where the
|
||||||
|
** function can yield.
|
||||||
|
*/
|
||||||
|
#if !defined(luai_threadyield)
|
||||||
|
#define luai_threadyield(L) {lua_unlock(L); lua_lock(L);}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** these macros allow user-specific actions when a thread is
|
||||||
|
** created/deleted/resumed/yielded.
|
||||||
|
*/
|
||||||
|
#if !defined(luai_userstateopen)
|
||||||
|
#define luai_userstateopen(L) ((void)L)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !defined(luai_userstateclose)
|
||||||
|
#define luai_userstateclose(L) ((void)L)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !defined(luai_userstatethread)
|
||||||
|
#define luai_userstatethread(L,L1) ((void)L)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !defined(luai_userstatefree)
|
||||||
|
#define luai_userstatefree(L,L1) ((void)L)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !defined(luai_userstateresume)
|
||||||
|
#define luai_userstateresume(L,n) ((void)L)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !defined(luai_userstateyield)
|
||||||
|
#define luai_userstateyield(L,n) ((void)L)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** The luai_num* macros define the primitive operations over numbers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* floor division (defined as 'floor(a/b)') */
|
||||||
|
#if !defined(luai_numidiv)
|
||||||
|
#define luai_numidiv(L,a,b) ((void)L, l_floor(luai_numdiv(L,a,b)))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* float division */
|
||||||
|
#if !defined(luai_numdiv)
|
||||||
|
#define luai_numdiv(L,a,b) ((a)/(b))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
** modulo: defined as 'a - floor(a/b)*b'; the direct computation
|
||||||
|
** using this definition has several problems with rounding errors,
|
||||||
|
** so it is better to use 'fmod'. 'fmod' gives the result of
|
||||||
|
** 'a - trunc(a/b)*b', and therefore must be corrected when
|
||||||
|
** 'trunc(a/b) ~= floor(a/b)'. That happens when the division has a
|
||||||
|
** non-integer negative result: non-integer result is equivalent to
|
||||||
|
** a non-zero remainder 'm'; negative result is equivalent to 'a' and
|
||||||
|
** 'b' with different signs, or 'm' and 'b' with different signs
|
||||||
|
** (as the result 'm' of 'fmod' has the same sign of 'a').
|
||||||
|
*/
|
||||||
|
#if !defined(luai_nummod)
|
||||||
|
#define luai_nummod(L,a,b,m) \
|
||||||
|
{ (void)L; (m) = l_mathop(fmod)(a,b); \
|
||||||
|
if (((m) > 0) ? (b) < 0 : ((m) < 0 && (b) > 0)) (m) += (b); }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* exponentiation */
|
||||||
|
#if !defined(luai_numpow)
|
||||||
|
#define luai_numpow(L,a,b) \
|
||||||
|
((void)L, (b == 2) ? (a)*(a) : l_mathop(pow)(a,b))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* the others are quite standard operations */
|
||||||
|
#if !defined(luai_numadd)
|
||||||
|
#define luai_numadd(L,a,b) ((a)+(b))
|
||||||
|
#define luai_numsub(L,a,b) ((a)-(b))
|
||||||
|
#define luai_nummul(L,a,b) ((a)*(b))
|
||||||
|
#define luai_numunm(L,a) (-(a))
|
||||||
|
#define luai_numeq(a,b) ((a)==(b))
|
||||||
|
#define luai_numlt(a,b) ((a)<(b))
|
||||||
|
#define luai_numle(a,b) ((a)<=(b))
|
||||||
|
#define luai_numgt(a,b) ((a)>(b))
|
||||||
|
#define luai_numge(a,b) ((a)>=(b))
|
||||||
|
#define luai_numisnan(a) (!luai_numeq((a), (a)))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** macro to control inclusion of some hard tests on stack reallocation
|
||||||
|
*/
|
||||||
|
#if !defined(HARDSTACKTESTS)
|
||||||
|
#define condmovestack(L,pre,pos) ((void)0)
|
||||||
|
#else
|
||||||
|
/* realloc stack keeping its size */
|
||||||
|
#define condmovestack(L,pre,pos) \
|
||||||
|
{ int sz_ = stacksize(L); pre; luaD_reallocstack((L), sz_, 0); pos; }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !defined(HARDMEMTESTS)
|
||||||
|
#define condchangemem(L,pre,pos) ((void)0)
|
||||||
|
#else
|
||||||
|
#define condchangemem(L,pre,pos) \
|
||||||
|
{ if (gcrunning(G(L))) { pre; luaC_fullgc(L, 0); pos; } }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,781 @@
|
|||||||
|
/*
|
||||||
|
** $Id: lmathlib.c $
|
||||||
|
** Standard mathematical library
|
||||||
|
** See Copyright Notice in lua.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define lmathlib_c
|
||||||
|
#define LUA_LIB
|
||||||
|
|
||||||
|
#include "lprefix.h"
|
||||||
|
|
||||||
|
|
||||||
|
#include <float.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#include "lua.h"
|
||||||
|
|
||||||
|
#include "lauxlib.h"
|
||||||
|
#include "lualib.h"
|
||||||
|
|
||||||
|
|
||||||
|
#undef PI
|
||||||
|
#define PI (l_mathop(3.141592653589793238462643383279502884))
|
||||||
|
|
||||||
|
|
||||||
|
static int math_abs (lua_State *L) {
|
||||||
|
if (lua_isinteger(L, 1)) {
|
||||||
|
lua_Integer n = lua_tointeger(L, 1);
|
||||||
|
if (n < 0) n = (lua_Integer)(0u - (lua_Unsigned)n);
|
||||||
|
lua_pushinteger(L, n);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
lua_pushnumber(L, l_mathop(fabs)(luaL_checknumber(L, 1)));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int math_sin (lua_State *L) {
|
||||||
|
lua_pushnumber(L, l_mathop(sin)(luaL_checknumber(L, 1)));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int math_cos (lua_State *L) {
|
||||||
|
lua_pushnumber(L, l_mathop(cos)(luaL_checknumber(L, 1)));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int math_tan (lua_State *L) {
|
||||||
|
lua_pushnumber(L, l_mathop(tan)(luaL_checknumber(L, 1)));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int math_asin (lua_State *L) {
|
||||||
|
lua_pushnumber(L, l_mathop(asin)(luaL_checknumber(L, 1)));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int math_acos (lua_State *L) {
|
||||||
|
lua_pushnumber(L, l_mathop(acos)(luaL_checknumber(L, 1)));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int math_atan (lua_State *L) {
|
||||||
|
lua_Number y = luaL_checknumber(L, 1);
|
||||||
|
lua_Number x = luaL_optnumber(L, 2, 1);
|
||||||
|
lua_pushnumber(L, l_mathop(atan2)(y, x));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int math_toint (lua_State *L) {
|
||||||
|
int valid;
|
||||||
|
lua_Integer n = lua_tointegerx(L, 1, &valid);
|
||||||
|
if (l_likely(valid))
|
||||||
|
lua_pushinteger(L, n);
|
||||||
|
else {
|
||||||
|
luaL_checkany(L, 1);
|
||||||
|
luaL_pushfail(L); /* value is not convertible to integer */
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void pushnumint (lua_State *L, lua_Number d) {
|
||||||
|
lua_Integer n;
|
||||||
|
if (lua_numbertointeger(d, &n)) /* does 'd' fit in an integer? */
|
||||||
|
lua_pushinteger(L, n); /* result is integer */
|
||||||
|
else
|
||||||
|
lua_pushnumber(L, d); /* result is float */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int math_floor (lua_State *L) {
|
||||||
|
if (lua_isinteger(L, 1))
|
||||||
|
lua_settop(L, 1); /* integer is its own floor */
|
||||||
|
else {
|
||||||
|
lua_Number d = l_mathop(floor)(luaL_checknumber(L, 1));
|
||||||
|
pushnumint(L, d);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int math_ceil (lua_State *L) {
|
||||||
|
if (lua_isinteger(L, 1))
|
||||||
|
lua_settop(L, 1); /* integer is its own ceil */
|
||||||
|
else {
|
||||||
|
lua_Number d = l_mathop(ceil)(luaL_checknumber(L, 1));
|
||||||
|
pushnumint(L, d);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int math_fmod (lua_State *L) {
|
||||||
|
if (lua_isinteger(L, 1) && lua_isinteger(L, 2)) {
|
||||||
|
lua_Integer d = lua_tointeger(L, 2);
|
||||||
|
if ((lua_Unsigned)d + 1u <= 1u) { /* special cases: -1 or 0 */
|
||||||
|
luaL_argcheck(L, d != 0, 2, "zero");
|
||||||
|
lua_pushinteger(L, 0); /* avoid overflow with 0x80000... / -1 */
|
||||||
|
}
|
||||||
|
else
|
||||||
|
lua_pushinteger(L, lua_tointeger(L, 1) % d);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
lua_pushnumber(L, l_mathop(fmod)(luaL_checknumber(L, 1),
|
||||||
|
luaL_checknumber(L, 2)));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** next function does not use 'modf', avoiding problems with 'double*'
|
||||||
|
** (which is not compatible with 'float*') when lua_Number is not
|
||||||
|
** 'double'.
|
||||||
|
*/
|
||||||
|
static int math_modf (lua_State *L) {
|
||||||
|
if (lua_isinteger(L ,1)) {
|
||||||
|
lua_settop(L, 1); /* number is its own integer part */
|
||||||
|
lua_pushnumber(L, 0); /* no fractional part */
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
lua_Number n = luaL_checknumber(L, 1);
|
||||||
|
/* integer part (rounds toward zero) */
|
||||||
|
lua_Number ip = (n < 0) ? l_mathop(ceil)(n) : l_mathop(floor)(n);
|
||||||
|
pushnumint(L, ip);
|
||||||
|
/* fractional part (test needed for inf/-inf) */
|
||||||
|
lua_pushnumber(L, (n == ip) ? l_mathop(0.0) : (n - ip));
|
||||||
|
}
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int math_sqrt (lua_State *L) {
|
||||||
|
lua_pushnumber(L, l_mathop(sqrt)(luaL_checknumber(L, 1)));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int math_ult (lua_State *L) {
|
||||||
|
lua_Integer a = luaL_checkinteger(L, 1);
|
||||||
|
lua_Integer b = luaL_checkinteger(L, 2);
|
||||||
|
lua_pushboolean(L, (lua_Unsigned)a < (lua_Unsigned)b);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int math_log (lua_State *L) {
|
||||||
|
lua_Number x = luaL_checknumber(L, 1);
|
||||||
|
lua_Number res;
|
||||||
|
if (lua_isnoneornil(L, 2))
|
||||||
|
res = l_mathop(log)(x);
|
||||||
|
else {
|
||||||
|
lua_Number base = luaL_checknumber(L, 2);
|
||||||
|
#if !defined(LUA_USE_C89)
|
||||||
|
if (base == l_mathop(2.0))
|
||||||
|
res = l_mathop(log2)(x);
|
||||||
|
else
|
||||||
|
#endif
|
||||||
|
if (base == l_mathop(10.0))
|
||||||
|
res = l_mathop(log10)(x);
|
||||||
|
else
|
||||||
|
res = l_mathop(log)(x)/l_mathop(log)(base);
|
||||||
|
}
|
||||||
|
lua_pushnumber(L, res);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int math_exp (lua_State *L) {
|
||||||
|
lua_pushnumber(L, l_mathop(exp)(luaL_checknumber(L, 1)));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int math_deg (lua_State *L) {
|
||||||
|
lua_pushnumber(L, luaL_checknumber(L, 1) * (l_mathop(180.0) / PI));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int math_rad (lua_State *L) {
|
||||||
|
lua_pushnumber(L, luaL_checknumber(L, 1) * (PI / l_mathop(180.0)));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int math_min (lua_State *L) {
|
||||||
|
int n = lua_gettop(L); /* number of arguments */
|
||||||
|
int imin = 1; /* index of current minimum value */
|
||||||
|
int i;
|
||||||
|
luaL_argcheck(L, n >= 1, 1, "value expected");
|
||||||
|
for (i = 2; i <= n; i++) {
|
||||||
|
if (lua_compare(L, i, imin, LUA_OPLT))
|
||||||
|
imin = i;
|
||||||
|
}
|
||||||
|
lua_pushvalue(L, imin);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int math_max (lua_State *L) {
|
||||||
|
int n = lua_gettop(L); /* number of arguments */
|
||||||
|
int imax = 1; /* index of current maximum value */
|
||||||
|
int i;
|
||||||
|
luaL_argcheck(L, n >= 1, 1, "value expected");
|
||||||
|
for (i = 2; i <= n; i++) {
|
||||||
|
if (lua_compare(L, imax, i, LUA_OPLT))
|
||||||
|
imax = i;
|
||||||
|
}
|
||||||
|
lua_pushvalue(L, imax);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int math_type (lua_State *L) {
|
||||||
|
if (lua_type(L, 1) == LUA_TNUMBER)
|
||||||
|
lua_pushstring(L, (lua_isinteger(L, 1)) ? "integer" : "float");
|
||||||
|
else {
|
||||||
|
luaL_checkany(L, 1);
|
||||||
|
luaL_pushfail(L);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** {==================================================================
|
||||||
|
** Pseudo-Random Number Generator based on 'xoshiro256**'.
|
||||||
|
** ===================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
** This code uses lots of shifts. ANSI C does not allow shifts greater
|
||||||
|
** than or equal to the width of the type being shifted, so some shifts
|
||||||
|
** are written in convoluted ways to match that restriction. For
|
||||||
|
** preprocessor tests, it assumes a width of 32 bits, so the maximum
|
||||||
|
** shift there is 31 bits.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/* number of binary digits in the mantissa of a float */
|
||||||
|
#define FIGS l_floatatt(MANT_DIG)
|
||||||
|
|
||||||
|
#if FIGS > 64
|
||||||
|
/* there are only 64 random bits; use them all */
|
||||||
|
#undef FIGS
|
||||||
|
#define FIGS 64
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** LUA_RAND32 forces the use of 32-bit integers in the implementation
|
||||||
|
** of the PRN generator (mainly for testing).
|
||||||
|
*/
|
||||||
|
#if !defined(LUA_RAND32) && !defined(Rand64)
|
||||||
|
|
||||||
|
/* try to find an integer type with at least 64 bits */
|
||||||
|
|
||||||
|
#if ((ULONG_MAX >> 31) >> 31) >= 3
|
||||||
|
|
||||||
|
/* 'long' has at least 64 bits */
|
||||||
|
#define Rand64 unsigned long
|
||||||
|
#define SRand64 long
|
||||||
|
|
||||||
|
#elif !defined(LUA_USE_C89) && defined(LLONG_MAX)
|
||||||
|
|
||||||
|
/* there is a 'long long' type (which must have at least 64 bits) */
|
||||||
|
#define Rand64 unsigned long long
|
||||||
|
#define SRand64 long long
|
||||||
|
|
||||||
|
#elif ((LUA_MAXUNSIGNED >> 31) >> 31) >= 3
|
||||||
|
|
||||||
|
/* 'lua_Unsigned' has at least 64 bits */
|
||||||
|
#define Rand64 lua_Unsigned
|
||||||
|
#define SRand64 lua_Integer
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
#if defined(Rand64) /* { */
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Standard implementation, using 64-bit integers.
|
||||||
|
** If 'Rand64' has more than 64 bits, the extra bits do not interfere
|
||||||
|
** with the 64 initial bits, except in a right shift. Moreover, the
|
||||||
|
** final result has to discard the extra bits.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* avoid using extra bits when needed */
|
||||||
|
#define trim64(x) ((x) & 0xffffffffffffffffu)
|
||||||
|
|
||||||
|
|
||||||
|
/* rotate left 'x' by 'n' bits */
|
||||||
|
static Rand64 rotl (Rand64 x, int n) {
|
||||||
|
return (x << n) | (trim64(x) >> (64 - n));
|
||||||
|
}
|
||||||
|
|
||||||
|
static Rand64 nextrand (Rand64 *state) {
|
||||||
|
Rand64 state0 = state[0];
|
||||||
|
Rand64 state1 = state[1];
|
||||||
|
Rand64 state2 = state[2] ^ state0;
|
||||||
|
Rand64 state3 = state[3] ^ state1;
|
||||||
|
Rand64 res = rotl(state1 * 5, 7) * 9;
|
||||||
|
state[0] = state0 ^ state3;
|
||||||
|
state[1] = state1 ^ state2;
|
||||||
|
state[2] = state2 ^ (state1 << 17);
|
||||||
|
state[3] = rotl(state3, 45);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Convert bits from a random integer into a float in the
|
||||||
|
** interval [0,1), getting the higher FIG bits from the
|
||||||
|
** random unsigned integer and converting that to a float.
|
||||||
|
** Some old Microsoft compilers cannot cast an unsigned long
|
||||||
|
** to a floating-point number, so we use a signed long as an
|
||||||
|
** intermediary. When lua_Number is float or double, the shift ensures
|
||||||
|
** that 'sx' is non negative; in that case, a good compiler will remove
|
||||||
|
** the correction.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* must throw out the extra (64 - FIGS) bits */
|
||||||
|
#define shift64_FIG (64 - FIGS)
|
||||||
|
|
||||||
|
/* 2^(-FIGS) == 2^-1 / 2^(FIGS-1) */
|
||||||
|
#define scaleFIG (l_mathop(0.5) / ((Rand64)1 << (FIGS - 1)))
|
||||||
|
|
||||||
|
static lua_Number I2d (Rand64 x) {
|
||||||
|
SRand64 sx = (SRand64)(trim64(x) >> shift64_FIG);
|
||||||
|
lua_Number res = (lua_Number)(sx) * scaleFIG;
|
||||||
|
if (sx < 0)
|
||||||
|
res += l_mathop(1.0); /* correct the two's complement if negative */
|
||||||
|
lua_assert(0 <= res && res < 1);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* convert a 'Rand64' to a 'lua_Unsigned' */
|
||||||
|
#define I2UInt(x) ((lua_Unsigned)trim64(x))
|
||||||
|
|
||||||
|
/* convert a 'lua_Unsigned' to a 'Rand64' */
|
||||||
|
#define Int2I(x) ((Rand64)(x))
|
||||||
|
|
||||||
|
|
||||||
|
#else /* no 'Rand64' }{ */
|
||||||
|
|
||||||
|
/* get an integer with at least 32 bits */
|
||||||
|
#if LUAI_IS32INT
|
||||||
|
typedef unsigned int lu_int32;
|
||||||
|
#else
|
||||||
|
typedef unsigned long lu_int32;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Use two 32-bit integers to represent a 64-bit quantity.
|
||||||
|
*/
|
||||||
|
typedef struct Rand64 {
|
||||||
|
lu_int32 h; /* higher half */
|
||||||
|
lu_int32 l; /* lower half */
|
||||||
|
} Rand64;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** If 'lu_int32' has more than 32 bits, the extra bits do not interfere
|
||||||
|
** with the 32 initial bits, except in a right shift and comparisons.
|
||||||
|
** Moreover, the final result has to discard the extra bits.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* avoid using extra bits when needed */
|
||||||
|
#define trim32(x) ((x) & 0xffffffffu)
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** basic operations on 'Rand64' values
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* build a new Rand64 value */
|
||||||
|
static Rand64 packI (lu_int32 h, lu_int32 l) {
|
||||||
|
Rand64 result;
|
||||||
|
result.h = h;
|
||||||
|
result.l = l;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* return i << n */
|
||||||
|
static Rand64 Ishl (Rand64 i, int n) {
|
||||||
|
lua_assert(n > 0 && n < 32);
|
||||||
|
return packI((i.h << n) | (trim32(i.l) >> (32 - n)), i.l << n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* i1 ^= i2 */
|
||||||
|
static void Ixor (Rand64 *i1, Rand64 i2) {
|
||||||
|
i1->h ^= i2.h;
|
||||||
|
i1->l ^= i2.l;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* return i1 + i2 */
|
||||||
|
static Rand64 Iadd (Rand64 i1, Rand64 i2) {
|
||||||
|
Rand64 result = packI(i1.h + i2.h, i1.l + i2.l);
|
||||||
|
if (trim32(result.l) < trim32(i1.l)) /* carry? */
|
||||||
|
result.h++;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* return i * 5 */
|
||||||
|
static Rand64 times5 (Rand64 i) {
|
||||||
|
return Iadd(Ishl(i, 2), i); /* i * 5 == (i << 2) + i */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* return i * 9 */
|
||||||
|
static Rand64 times9 (Rand64 i) {
|
||||||
|
return Iadd(Ishl(i, 3), i); /* i * 9 == (i << 3) + i */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* return 'i' rotated left 'n' bits */
|
||||||
|
static Rand64 rotl (Rand64 i, int n) {
|
||||||
|
lua_assert(n > 0 && n < 32);
|
||||||
|
return packI((i.h << n) | (trim32(i.l) >> (32 - n)),
|
||||||
|
(trim32(i.h) >> (32 - n)) | (i.l << n));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* for offsets larger than 32, rotate right by 64 - offset */
|
||||||
|
static Rand64 rotl1 (Rand64 i, int n) {
|
||||||
|
lua_assert(n > 32 && n < 64);
|
||||||
|
n = 64 - n;
|
||||||
|
return packI((trim32(i.h) >> n) | (i.l << (32 - n)),
|
||||||
|
(i.h << (32 - n)) | (trim32(i.l) >> n));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** implementation of 'xoshiro256**' algorithm on 'Rand64' values
|
||||||
|
*/
|
||||||
|
static Rand64 nextrand (Rand64 *state) {
|
||||||
|
Rand64 res = times9(rotl(times5(state[1]), 7));
|
||||||
|
Rand64 t = Ishl(state[1], 17);
|
||||||
|
Ixor(&state[2], state[0]);
|
||||||
|
Ixor(&state[3], state[1]);
|
||||||
|
Ixor(&state[1], state[2]);
|
||||||
|
Ixor(&state[0], state[3]);
|
||||||
|
Ixor(&state[2], t);
|
||||||
|
state[3] = rotl1(state[3], 45);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Converts a 'Rand64' into a float.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* an unsigned 1 with proper type */
|
||||||
|
#define UONE ((lu_int32)1)
|
||||||
|
|
||||||
|
|
||||||
|
#if FIGS <= 32
|
||||||
|
|
||||||
|
/* 2^(-FIGS) */
|
||||||
|
#define scaleFIG (l_mathop(0.5) / (UONE << (FIGS - 1)))
|
||||||
|
|
||||||
|
/*
|
||||||
|
** get up to 32 bits from higher half, shifting right to
|
||||||
|
** throw out the extra bits.
|
||||||
|
*/
|
||||||
|
static lua_Number I2d (Rand64 x) {
|
||||||
|
lua_Number h = (lua_Number)(trim32(x.h) >> (32 - FIGS));
|
||||||
|
return h * scaleFIG;
|
||||||
|
}
|
||||||
|
|
||||||
|
#else /* 32 < FIGS <= 64 */
|
||||||
|
|
||||||
|
/* 2^(-FIGS) = 1.0 / 2^30 / 2^3 / 2^(FIGS-33) */
|
||||||
|
#define scaleFIG \
|
||||||
|
(l_mathop(1.0) / (UONE << 30) / l_mathop(8.0) / (UONE << (FIGS - 33)))
|
||||||
|
|
||||||
|
/*
|
||||||
|
** use FIGS - 32 bits from lower half, throwing out the other
|
||||||
|
** (32 - (FIGS - 32)) = (64 - FIGS) bits
|
||||||
|
*/
|
||||||
|
#define shiftLOW (64 - FIGS)
|
||||||
|
|
||||||
|
/*
|
||||||
|
** higher 32 bits go after those (FIGS - 32) bits: shiftHI = 2^(FIGS - 32)
|
||||||
|
*/
|
||||||
|
#define shiftHI ((lua_Number)(UONE << (FIGS - 33)) * l_mathop(2.0))
|
||||||
|
|
||||||
|
|
||||||
|
static lua_Number I2d (Rand64 x) {
|
||||||
|
lua_Number h = (lua_Number)trim32(x.h) * shiftHI;
|
||||||
|
lua_Number l = (lua_Number)(trim32(x.l) >> shiftLOW);
|
||||||
|
return (h + l) * scaleFIG;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/* convert a 'Rand64' to a 'lua_Unsigned' */
|
||||||
|
static lua_Unsigned I2UInt (Rand64 x) {
|
||||||
|
return (((lua_Unsigned)trim32(x.h) << 31) << 1) | (lua_Unsigned)trim32(x.l);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* convert a 'lua_Unsigned' to a 'Rand64' */
|
||||||
|
static Rand64 Int2I (lua_Unsigned n) {
|
||||||
|
return packI((lu_int32)((n >> 31) >> 1), (lu_int32)n);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* } */
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** A state uses four 'Rand64' values.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
Rand64 s[4];
|
||||||
|
} RanState;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Project the random integer 'ran' into the interval [0, n].
|
||||||
|
** Because 'ran' has 2^B possible values, the projection can only be
|
||||||
|
** uniform when the size of the interval is a power of 2 (exact
|
||||||
|
** division). Otherwise, to get a uniform projection into [0, n], we
|
||||||
|
** first compute 'lim', the smallest Mersenne number not smaller than
|
||||||
|
** 'n'. We then project 'ran' into the interval [0, lim]. If the result
|
||||||
|
** is inside [0, n], we are done. Otherwise, we try with another 'ran',
|
||||||
|
** until we have a result inside the interval.
|
||||||
|
*/
|
||||||
|
static lua_Unsigned project (lua_Unsigned ran, lua_Unsigned n,
|
||||||
|
RanState *state) {
|
||||||
|
if ((n & (n + 1)) == 0) /* is 'n + 1' a power of 2? */
|
||||||
|
return ran & n; /* no bias */
|
||||||
|
else {
|
||||||
|
lua_Unsigned lim = n;
|
||||||
|
/* compute the smallest (2^b - 1) not smaller than 'n' */
|
||||||
|
lim |= (lim >> 1);
|
||||||
|
lim |= (lim >> 2);
|
||||||
|
lim |= (lim >> 4);
|
||||||
|
lim |= (lim >> 8);
|
||||||
|
lim |= (lim >> 16);
|
||||||
|
#if (LUA_MAXUNSIGNED >> 31) >= 3
|
||||||
|
lim |= (lim >> 32); /* integer type has more than 32 bits */
|
||||||
|
#endif
|
||||||
|
lua_assert((lim & (lim + 1)) == 0 /* 'lim + 1' is a power of 2, */
|
||||||
|
&& lim >= n /* not smaller than 'n', */
|
||||||
|
&& (lim >> 1) < n); /* and it is the smallest one */
|
||||||
|
while ((ran &= lim) > n) /* project 'ran' into [0..lim] */
|
||||||
|
ran = I2UInt(nextrand(state->s)); /* not inside [0..n]? try again */
|
||||||
|
return ran;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int math_random (lua_State *L) {
|
||||||
|
lua_Integer low, up;
|
||||||
|
lua_Unsigned p;
|
||||||
|
RanState *state = (RanState *)lua_touserdata(L, lua_upvalueindex(1));
|
||||||
|
Rand64 rv = nextrand(state->s); /* next pseudo-random value */
|
||||||
|
switch (lua_gettop(L)) { /* check number of arguments */
|
||||||
|
case 0: { /* no arguments */
|
||||||
|
lua_pushnumber(L, I2d(rv)); /* float between 0 and 1 */
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
case 1: { /* only upper limit */
|
||||||
|
low = 1;
|
||||||
|
up = luaL_checkinteger(L, 1);
|
||||||
|
if (up == 0) { /* single 0 as argument? */
|
||||||
|
lua_pushinteger(L, I2UInt(rv)); /* full random integer */
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 2: { /* lower and upper limits */
|
||||||
|
low = luaL_checkinteger(L, 1);
|
||||||
|
up = luaL_checkinteger(L, 2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: return luaL_error(L, "wrong number of arguments");
|
||||||
|
}
|
||||||
|
/* random integer in the interval [low, up] */
|
||||||
|
luaL_argcheck(L, low <= up, 1, "interval is empty");
|
||||||
|
/* project random integer into the interval [0, up - low] */
|
||||||
|
p = project(I2UInt(rv), (lua_Unsigned)up - (lua_Unsigned)low, state);
|
||||||
|
lua_pushinteger(L, p + (lua_Unsigned)low);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void setseed (lua_State *L, Rand64 *state,
|
||||||
|
lua_Unsigned n1, lua_Unsigned n2) {
|
||||||
|
int i;
|
||||||
|
state[0] = Int2I(n1);
|
||||||
|
state[1] = Int2I(0xff); /* avoid a zero state */
|
||||||
|
state[2] = Int2I(n2);
|
||||||
|
state[3] = Int2I(0);
|
||||||
|
for (i = 0; i < 16; i++)
|
||||||
|
nextrand(state); /* discard initial values to "spread" seed */
|
||||||
|
lua_pushinteger(L, n1);
|
||||||
|
lua_pushinteger(L, n2);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Set a "random" seed. To get some randomness, use the current time
|
||||||
|
** and the address of 'L' (in case the machine does address space layout
|
||||||
|
** randomization).
|
||||||
|
*/
|
||||||
|
static void randseed (lua_State *L, RanState *state) {
|
||||||
|
lua_Unsigned seed1 = (lua_Unsigned)time(NULL);
|
||||||
|
lua_Unsigned seed2 = (lua_Unsigned)(size_t)L;
|
||||||
|
setseed(L, state->s, seed1, seed2);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int math_randomseed (lua_State *L) {
|
||||||
|
RanState *state = (RanState *)lua_touserdata(L, lua_upvalueindex(1));
|
||||||
|
if (lua_isnone(L, 1)) {
|
||||||
|
randseed(L, state);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
lua_Integer n1 = luaL_checkinteger(L, 1);
|
||||||
|
lua_Integer n2 = luaL_optinteger(L, 2, 0);
|
||||||
|
setseed(L, state->s, n1, n2);
|
||||||
|
}
|
||||||
|
return 2; /* return seeds */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static const luaL_Reg randfuncs[] = {
|
||||||
|
{"random", math_random},
|
||||||
|
{"randomseed", math_randomseed},
|
||||||
|
{NULL, NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Register the random functions and initialize their state.
|
||||||
|
*/
|
||||||
|
static void setrandfunc (lua_State *L) {
|
||||||
|
RanState *state = (RanState *)lua_newuserdatauv(L, sizeof(RanState), 0);
|
||||||
|
randseed(L, state); /* initialize with a "random" seed */
|
||||||
|
lua_pop(L, 2); /* remove pushed seeds */
|
||||||
|
luaL_setfuncs(L, randfuncs, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* }================================================================== */
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** {==================================================================
|
||||||
|
** Deprecated functions (for compatibility only)
|
||||||
|
** ===================================================================
|
||||||
|
*/
|
||||||
|
#if defined(LUA_COMPAT_MATHLIB)
|
||||||
|
|
||||||
|
static int math_cosh (lua_State *L) {
|
||||||
|
lua_pushnumber(L, l_mathop(cosh)(luaL_checknumber(L, 1)));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int math_sinh (lua_State *L) {
|
||||||
|
lua_pushnumber(L, l_mathop(sinh)(luaL_checknumber(L, 1)));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int math_tanh (lua_State *L) {
|
||||||
|
lua_pushnumber(L, l_mathop(tanh)(luaL_checknumber(L, 1)));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int math_pow (lua_State *L) {
|
||||||
|
lua_Number x = luaL_checknumber(L, 1);
|
||||||
|
lua_Number y = luaL_checknumber(L, 2);
|
||||||
|
lua_pushnumber(L, l_mathop(pow)(x, y));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int math_frexp (lua_State *L) {
|
||||||
|
int e;
|
||||||
|
lua_pushnumber(L, l_mathop(frexp)(luaL_checknumber(L, 1), &e));
|
||||||
|
lua_pushinteger(L, e);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int math_ldexp (lua_State *L) {
|
||||||
|
lua_Number x = luaL_checknumber(L, 1);
|
||||||
|
int ep = (int)luaL_checkinteger(L, 2);
|
||||||
|
lua_pushnumber(L, l_mathop(ldexp)(x, ep));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int math_log10 (lua_State *L) {
|
||||||
|
lua_pushnumber(L, l_mathop(log10)(luaL_checknumber(L, 1)));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
/* }================================================================== */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
static const luaL_Reg mathlib[] = {
|
||||||
|
{"abs", math_abs},
|
||||||
|
{"acos", math_acos},
|
||||||
|
{"asin", math_asin},
|
||||||
|
{"atan", math_atan},
|
||||||
|
{"ceil", math_ceil},
|
||||||
|
{"cos", math_cos},
|
||||||
|
{"deg", math_deg},
|
||||||
|
{"exp", math_exp},
|
||||||
|
{"tointeger", math_toint},
|
||||||
|
{"floor", math_floor},
|
||||||
|
{"fmod", math_fmod},
|
||||||
|
{"ult", math_ult},
|
||||||
|
{"log", math_log},
|
||||||
|
{"max", math_max},
|
||||||
|
{"min", math_min},
|
||||||
|
{"modf", math_modf},
|
||||||
|
{"rad", math_rad},
|
||||||
|
{"sin", math_sin},
|
||||||
|
{"sqrt", math_sqrt},
|
||||||
|
{"tan", math_tan},
|
||||||
|
{"type", math_type},
|
||||||
|
#if defined(LUA_COMPAT_MATHLIB)
|
||||||
|
{"atan2", math_atan},
|
||||||
|
{"cosh", math_cosh},
|
||||||
|
{"sinh", math_sinh},
|
||||||
|
{"tanh", math_tanh},
|
||||||
|
{"pow", math_pow},
|
||||||
|
{"frexp", math_frexp},
|
||||||
|
{"ldexp", math_ldexp},
|
||||||
|
{"log10", math_log10},
|
||||||
|
#endif
|
||||||
|
/* placeholders */
|
||||||
|
{"random", NULL},
|
||||||
|
{"randomseed", NULL},
|
||||||
|
{"pi", NULL},
|
||||||
|
{"huge", NULL},
|
||||||
|
{"maxinteger", NULL},
|
||||||
|
{"mininteger", NULL},
|
||||||
|
{NULL, NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Open math library
|
||||||
|
*/
|
||||||
|
LUAMOD_API int luaopen_math (lua_State *L) {
|
||||||
|
luaL_newlib(L, mathlib);
|
||||||
|
lua_pushnumber(L, PI);
|
||||||
|
lua_setfield(L, -2, "pi");
|
||||||
|
lua_pushnumber(L, (lua_Number)HUGE_VAL);
|
||||||
|
lua_setfield(L, -2, "huge");
|
||||||
|
lua_pushinteger(L, LUA_MAXINTEGER);
|
||||||
|
lua_setfield(L, -2, "maxinteger");
|
||||||
|
lua_pushinteger(L, LUA_MININTEGER);
|
||||||
|
lua_setfield(L, -2, "mininteger");
|
||||||
|
setrandfunc(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user