From 8d874ae31b149716b46f01783730d6724bba37c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Wed, 11 Mar 2026 00:03:58 +0100 Subject: [PATCH] =?UTF-8?q?feat(app):=20merge=20multi-copy=20work=20?= =?UTF-8?q?=E2=80=94=20modules,=20auth,=20UI=20amiga=20shell,=20scenarios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ci.yml | 35 + LICENSE.md | 3 + README.md | 234 +- README_ESP32_ZACUS.md | 156 +- boards/freenove_esp32_s3_wroom.json | 54 + docs/FNK0102H_SOURCE_OF_TRUTH.md | 73 + docs/QUICKSTART.md | 70 + .../include/ESP32CameraPins.h | 184 + .../include/ESP32QRCodeReader.h | 63 + lib/ESP32QRCodeReader_vendor/library.json | 22 + .../library.properties | 9 + .../src/ESP32QRCodeReader.cpp | 290 + .../src/openmv/collections.c | 85 + .../src/openmv/collections.h | 33 + .../src/openmv/fmath.h | 70 + .../src/quirc/LICENSE | 16 + .../src/quirc/README.md | 193 + .../src/quirc/decode.c | 983 ++++ .../src/quirc/identify.c | 1286 +++++ .../src/quirc/quirc.c | 104 + .../src/quirc/quirc.h | 178 + .../src/quirc/quirc_internal.h | 121 + .../src/quirc/version_db.c | 184 + lib/tinyexpr_vendor/LICENSE | 20 + lib/tinyexpr_vendor/README.md | 319 ++ lib/tinyexpr_vendor/library.json | 17 + lib/tinyexpr_vendor/src/tinyexpr.c | 734 +++ lib/tinyexpr_vendor/src/tinyexpr.h | 87 + .../freenove_esp32s3_app4mb_fs12096kb.csv | 6 + .../freenove_esp32s3_app4mb_fs3840kb.csv | 6 + platformio.ini | 221 +- scripts/audit_boot_log.py | 130 + scripts/bootstrap_platformio.sh | 41 + scripts/doctor_repo.sh | 110 + scripts/flash_monitor_audit.sh | 181 + scripts/serial_boot_capture.py | 175 + specs/apps/IMPLEMENTATION_AUDIT_TODO.md | 9 + tests/phase6_test.py | 13 +- tests/phase6_validation_smoke.sh | 9 +- tests/phase9_ui_validation.py | 118 +- tests/sprint1_utility_contract.py | 25 +- tests/sprint2_capture_contract.py | 27 +- tests/sprint3_audio_contract.py | 347 ++ ui_freenove_allinone/RC_FINAL_BOARD.md | 2 +- ui_freenove_allinone/README.md | 382 +- ui_freenove_allinone/include/app/app_module.h | 39 + .../include/app/app_registry.h | 51 +- .../include/app/app_runtime_manager.h | 68 +- .../include/app/default_app_registry.h | 27 + .../include/app/file_share_service.h | 1 - .../include/app/modules/audio_player_module.h | 20 + .../include/audio/audio_manager.h | 4 + .../include/core/mutex_manager.h | 162 +- ui_freenove_allinone/include/core/str_utils.h | 46 + .../include/drivers/board/hardware_manager.h | 1 + .../include/runtime/perf/perf_monitor.h | 3 + .../include/runtime/simd/simd_accel.h | 36 +- .../include/system/network/network_manager.h | 2 - .../include/ui/workbench/amiga_ui_shell.h | 131 + .../include/ui_freenove_config.h | 12 +- ui_freenove_allinone/src/app/app_audio.cpp | 15 +- .../src/app/app_calculator.cpp | 3 +- .../src/app/app_flashlight.cpp | 2 +- ui_freenove_allinone/src/app/app_registry.cpp | 323 +- .../src/app/app_runtime_manager.cpp | 531 +- ui_freenove_allinone/src/app/app_timer.cpp | 18 +- .../src/app/file_share_service.cpp | 27 +- .../src/app/modules/app_modules.cpp | 346 +- .../src/app/modules/audio_player_module.cpp | 66 + .../src/app/runtime_web_service 2.cpp | 12 - .../src/audio/audio_manager.cpp | 120 +- ui_freenove_allinone/src/audio_manager.cpp | 476 -- .../src/auth/auth_service.cpp | 17 +- ui_freenove_allinone/src/button_manager.cpp | 209 - .../src/camera/camera_manager.cpp | 97 +- ui_freenove_allinone/src/camera_manager.cpp | 332 -- .../src/core/mutex_manager.cpp | 293 +- .../src/drivers/board/hardware_manager.cpp | 19 + .../src/drivers/input/touch_emulator.cpp | 90 +- ui_freenove_allinone/src/hardware_manager.cpp | 926 --- ui_freenove_allinone/src/media_manager.cpp | 328 -- ui_freenove_allinone/src/network_manager.cpp | 981 ---- ui_freenove_allinone/src/placeholder.cpp | 2 - .../src/runtime/memory/caps_allocator.cpp | 12 +- .../src/runtime/perf/perf_monitor.cpp | 8 + .../src/runtime/runtime_config_service.cpp | 65 +- .../src/runtime/simd/simd_accel.cpp | 551 +- ui_freenove_allinone/src/scenario_manager.cpp | 670 --- ui_freenove_allinone/src/storage_manager.cpp | 802 --- .../src/system/media/media_manager.cpp | 54 +- .../src/system/network/network_manager.cpp | 127 +- ui_freenove_allinone/src/touch_manager.cpp | 29 - .../ui/fonts/lv_font_ibmplexmono_bold_12 2.c | 2366 -------- .../fonts/lv_font_ibmplexmono_italic_24 2.c | 5041 ----------------- .../src/ui/qr/qr_scene_controller.cpp | 21 +- .../src/ui/qr/qr_validation_rules.cpp | 25 +- ui_freenove_allinone/src/ui/ui_manager.cpp | 51 +- .../src/ui/ui_manager_display 2.cpp | 899 --- .../src/ui/ui_manager_intro 2.cpp | 2475 -------- .../src/ui/workbench/amiga_ui_shell.cpp | 514 ++ ui_freenove_allinone/src/ui_manager.cpp | 3063 ---------- 101 files changed, 9051 insertions(+), 20983 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 LICENSE.md create mode 100644 boards/freenove_esp32_s3_wroom.json create mode 100644 docs/FNK0102H_SOURCE_OF_TRUTH.md create mode 100644 docs/QUICKSTART.md create mode 100644 lib/ESP32QRCodeReader_vendor/include/ESP32CameraPins.h create mode 100644 lib/ESP32QRCodeReader_vendor/include/ESP32QRCodeReader.h create mode 100644 lib/ESP32QRCodeReader_vendor/library.json create mode 100644 lib/ESP32QRCodeReader_vendor/library.properties create mode 100644 lib/ESP32QRCodeReader_vendor/src/ESP32QRCodeReader.cpp create mode 100644 lib/ESP32QRCodeReader_vendor/src/openmv/collections.c create mode 100644 lib/ESP32QRCodeReader_vendor/src/openmv/collections.h create mode 100644 lib/ESP32QRCodeReader_vendor/src/openmv/fmath.h create mode 100644 lib/ESP32QRCodeReader_vendor/src/quirc/LICENSE create mode 100644 lib/ESP32QRCodeReader_vendor/src/quirc/README.md create mode 100644 lib/ESP32QRCodeReader_vendor/src/quirc/decode.c create mode 100644 lib/ESP32QRCodeReader_vendor/src/quirc/identify.c create mode 100644 lib/ESP32QRCodeReader_vendor/src/quirc/quirc.c create mode 100644 lib/ESP32QRCodeReader_vendor/src/quirc/quirc.h create mode 100644 lib/ESP32QRCodeReader_vendor/src/quirc/quirc_internal.h create mode 100644 lib/ESP32QRCodeReader_vendor/src/quirc/version_db.c create mode 100644 lib/tinyexpr_vendor/LICENSE create mode 100644 lib/tinyexpr_vendor/README.md create mode 100644 lib/tinyexpr_vendor/library.json create mode 100755 lib/tinyexpr_vendor/src/tinyexpr.c create mode 100644 lib/tinyexpr_vendor/src/tinyexpr.h create mode 100644 partitions/freenove_esp32s3_app4mb_fs12096kb.csv create mode 100644 partitions/freenove_esp32s3_app4mb_fs3840kb.csv create mode 100755 scripts/audit_boot_log.py create mode 100755 scripts/bootstrap_platformio.sh create mode 100755 scripts/doctor_repo.sh create mode 100755 scripts/flash_monitor_audit.sh create mode 100755 scripts/serial_boot_capture.py create mode 100644 specs/apps/IMPLEMENTATION_AUDIT_TODO.md create mode 100755 tests/sprint3_audio_contract.py create mode 100644 ui_freenove_allinone/include/app/app_module.h create mode 100644 ui_freenove_allinone/include/app/default_app_registry.h create mode 100644 ui_freenove_allinone/include/app/modules/audio_player_module.h create mode 100644 ui_freenove_allinone/include/core/str_utils.h create mode 100644 ui_freenove_allinone/include/ui/workbench/amiga_ui_shell.h create mode 100644 ui_freenove_allinone/src/app/modules/audio_player_module.cpp delete mode 100644 ui_freenove_allinone/src/app/runtime_web_service 2.cpp delete mode 100644 ui_freenove_allinone/src/audio_manager.cpp delete mode 100644 ui_freenove_allinone/src/button_manager.cpp delete mode 100644 ui_freenove_allinone/src/camera_manager.cpp delete mode 100644 ui_freenove_allinone/src/hardware_manager.cpp delete mode 100644 ui_freenove_allinone/src/media_manager.cpp delete mode 100644 ui_freenove_allinone/src/network_manager.cpp delete mode 100644 ui_freenove_allinone/src/placeholder.cpp delete mode 100644 ui_freenove_allinone/src/scenario_manager.cpp delete mode 100644 ui_freenove_allinone/src/storage_manager.cpp delete mode 100644 ui_freenove_allinone/src/touch_manager.cpp delete mode 100644 ui_freenove_allinone/src/ui/fonts/lv_font_ibmplexmono_bold_12 2.c delete mode 100644 ui_freenove_allinone/src/ui/fonts/lv_font_ibmplexmono_italic_24 2.c delete mode 100644 ui_freenove_allinone/src/ui/ui_manager_display 2.cpp delete mode 100644 ui_freenove_allinone/src/ui/ui_manager_intro 2.cpp create mode 100644 ui_freenove_allinone/src/ui/workbench/amiga_ui_shell.cpp delete mode 100644 ui_freenove_allinone/src/ui_manager.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..be960a8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + platformio: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + path: | + ~/.cache/pip + ~/.platformio/.cache + key: ${{ runner.os }}-pio + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install PlatformIO + run: python -m pip install --upgrade pip platformio + - name: Build + run: pio run + - name: Size report + run: pio run -t size diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..98b40ba --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,3 @@ +This Source Code Form is subject to the terms of the Mozilla Public License, +v. 2.0. If a copy of the MPL was not distributed with this file, You can +obtain one at https://mozilla.org/MPL/2.0/. diff --git a/README.md b/README.md index d086173..b77305f 100644 --- a/README.md +++ b/README.md @@ -1,218 +1,102 @@ -# Optimisation automatique hardware -Depuis février 2026, le cockpit détecte automatiquement le hardware connecté (port série, ID USB) et ne build/flash que l’environnement PlatformIO correspondant. +# ESP32_ZACUS -Pour builder ou flasher : -- Menu cockpit : "Build all firmware" ou "Flash auto (hardware détecté)" -- Ligne de commande : ./tools/dev/cockpit.sh flash (auto) ou ./tools/dev/cockpit.sh build (auto) +Workspace firmware centré sur une seule cible: Freenove FNK0102H avec `ui_freenove_allinone`. -La détection est basée sur pyserial et l’ID USB (Freenove, ESP32, ESP8266, RP2040). Le log affiche le hardware détecté et l’environnement PlatformIO ciblé. +## Docs canoniques -Pour forcer un build/flash spécifique : ./tools/dev/cockpit.sh flash +- `docs/QUICKSTART.md`: bootstrap, build, flash, monitor, smoke. +- `docs/FNK0102H_SOURCE_OF_TRUTH.md`: matrice board/config/pins/support. +- `README_ESP32_ZACUS.md`: notes runtime et signatures série utiles. -## Statut Freenove ESP32-S3 (2026-03-01) +## Arborescence utile -- Build firmware: OK (`freenove_esp32s3_full_with_ui`) -- Build/flash LittleFS: OK -- Flash firmware: OK -- Smoke série runtime: KO (reboot loop `rst:0x3`, `Saved PC:0x403cdb0a`) -- Détail et plan de debug: `README_ESP32_ZACUS.md` +- `platformio.ini`: env PlatformIO canonique `freenove_esp32s3_full_with_ui`. +- `ui_freenove_allinone/`: firmware UI/audio/caméra/réseau. +- `data/`: contenu LittleFS. +- `lib/`: bibliothèques runtime. +- `scripts/`: bootstrap et scripts repo. +- `tests/`: harness Sprint 1, Sprint 2, Sprint 3, Phase 9. -# Hardware Firmware +## Environnements supportés -> **[Mise à jour 2026]** -> -> **Tous les assets LittleFS (scénarios, écrans, scènes, audio, actions, etc.) sont désormais centralisés dans le dossier `data/` à la racine de `hardware/firmware/`.** -> -> Ce dossier unique sert de source pour le flash LittleFS sur ESP32, ESP8266 et RP2040. Les anciens dossiers `data/` dans les sous-projets doivent être migrés/supprimés (voir encart migration ci-dessous). +- `freenove_esp32s3_full_with_ui`: chemin release/stabilisation. +- `freenove_esp32s3`: alias du chemin canonique. +- `freenove_esp32s3_touch`: expérimental, hors release, sans CI. +## Démarrage rapide -Workspace PlatformIO unique pour 3 firmwares: - -- `esp32_audio/`: firmware principal audio -- `ui/esp8266_oled/`: UI OLED legere -- `ui/rp2040_tft/`: UI TFT tactile (LVGL) -- `protocol/`: contrat UART partage (`ui_link_v2.md`, `ui_link_v2.h`) -### 🟦 Freenove Media Kit (RP2040) - -Un environnement PlatformIO dédié est disponible pour le boîtier Freenove Media Kit : - -- **Build** : `pio run -e ui_rp2040_freenove` -- **Flash** : `pio run -e ui_rp2040_freenove -t upload --upload-port ` -- **Monitor** : `pio device monitor -e ui_rp2040_freenove --port ` - -Le mapping hardware (pins, écran, boutons) est défini dans `ui/rp2040_tft/include/ui_freenove_config.h`. - -**Schéma de branchement** : voir `hardware/wiring/wiring.md` (section Freenove) - -**Remarque** : adaptez les defines dans `ui_freenove_config.h` selon votre version du Media Kit (écran, boutons, etc.). - - -## 📚 Documentation - -- **[État des lieux](docs/STATE_ANALYSIS.md)** - Analyse complète du firmware -- **[Recommandations Sprint](docs/SPRINT_RECOMMENDATIONS.md)** - Actions prioritaires -- **[Recovery WiFi/AP & Health](docs/WIFI_RECOVERY_AND_HEALTH.md)** - Procédure recovery AP, healthcheck, troubleshooting - -Pour débuter : [docs/QUICKSTART.md](docs/QUICKSTART.md) - - -## Structure des assets LittleFS (cross-plateforme) - -``` -hardware/firmware/data/ - story/ - scenarios/ - DEFAULT.json - apps/ - screens/ - audio/ - actions/ - audio/ - radio/ - net/ +```bash +./scripts/bootstrap_platformio.sh +./scripts/doctor_repo.sh +source .venv/bin/activate +python -m platformio run -e freenove_esp32s3_full_with_ui +python -m platformio run -e freenove_esp32s3_full_with_ui -t buildfs +python -m platformio run -e freenove_esp32s3_full_with_ui -t uploadfs --upload-port +python -m platformio run -e freenove_esp32s3_full_with_ui -t upload --upload-port +python -m platformio device monitor -b 115200 --port ``` -**Scripts de génération et de flash : toujours pointer vers ce dossier.** +## Guardrails runtime ---- +- Si la PSRAM est détectée, le firmware démarre normalement puis autorise le réseau après validation UI. +- Si la PSRAM n'est pas détectée, le firmware bascule en `safe diagnostic mode`. +- En mode diagnostic, le firmware garde uniquement la pile série, l'affichage minimal et les boutons physiques. +- En mode diagnostic, caméra, FX, micro, audio, Wi-Fi, ESP-NOW, WebUI et partage de fichiers restent coupés. -## Build +## Vérifications minimales -```sh -pio run -``` - -Build cible: - -```sh -pio run -e esp32dev -pio run -e esp32_release -pio run -e esp8266_oled -pio run -e ui_rp2040_ili9488 -pio run -e ui_rp2040_ili9486 -``` - -Script global: - -```sh -./build_all.sh -``` - -Bootstrap local tools once: - -```sh -./tools/dev/bootstrap_local.sh -``` - -## Story portable (generation + runtime) - -- Generation library: `lib/zacus_story_gen_ai/` (Yamale + Jinja2). -- Runtime library: `lib/zacus_story_portable/` (tinyfsm-style internals). -- Story serial protocol: JSON-lines V3 (`story.*`), see `docs/protocols/story_v3_serial.md`. -- Canonical migration doc: `docs/STORY_PORTABILITY_MIGRATION.md`. - -CLI: - -```sh -./tools/dev/story-gen validate -./tools/dev/story-gen generate-cpp -./tools/dev/story-gen generate-bundle -./tools/dev/story-gen all +```bash +./scripts/doctor_repo.sh +python tests/sprint1_utility_contract.py --mode serial --port +python tests/sprint2_capture_contract.py --mode serial --port +python tests/sprint3_audio_contract.py --mode serial --port +python tests/phase9_ui_validation.py --port ``` -## Flash (cockpit) - -```sh -./tools/dev/cockpit.sh flash -``` -Options utiles: - -- `ZACUS_FLASH_ESP32_ENVS="esp32dev esp32_release"` -- `ZACUS_FLASH_RP2040_ENVS="ui_rp2040_ili9488 ui_rp2040_ili9486"` -- `ZACUS_PORT_ESP32=... ZACUS_PORT_ESP8266=... ZACUS_PORT_RP2040=...` ---- -## Migration LittleFS (2026) -- Déplacer tous les fichiers de scénario, écrans, scènes, audio, etc. dans `hardware/firmware/data/`. -- Adapter les scripts de génération et de flash pour pointer vers ce dossier. -- Supprimer les anciens dossiers `data/` dispersés dans les sous-projets (`ui/rp2040_tft/data/`, `esp32_audio/data/`, etc.). -- Mettre à jour tous les guides et onboarding pour refléter cette structure unique. ---- -```sh -make fast-esp32 ESP32_PORT= -make fast-ui-oled UI_OLED_PORT= -make fast-ui-tft UI_TFT_PORT= -``` -Smoke série (manuel): -```sh -python3 tools/dev/serial_smoke.py --role auto --baud 115200 --wait-port 3 --allow-no-hardware -``` -MacOS CP2102 duplicates share VID/PID=10C4:EA60/0001; the LOCATION (20-6.1.1=ESP32, 20-6.1.2=ESP8266) drives the detector. `tools/dev/ports_map.json` now uses `location -> role` and `vidpid -> role` mappings. -USB console monitoring uses `115200`. ESP8266 internal UI link SoftwareSerial stays at `57600` (internal link only). -## Serial smoke commands -- baseline smoke (auto handles already connected boards): `python3 tools/dev/serial_smoke.py --role auto --baud 115200 --wait-port 3 --allow-no-hardware` -- run every detected role: `python3 tools/dev/serial_smoke.py --role all --baud 115200 --wait-port 3 --allow-no-hardware` -- force hardware detection: `ZACUS_REQUIRE_HW=1 python3 tools/dev/serial_smoke.py --role auto --baud 115200 --wait-port 180` -- skip PlatformIO builds and just run smoke (useful when downloads are impossible): `ZACUS_SKIP_PIO=1 ./tools/dev/run_matrix_and_smoke.sh` -## Build + smoke combo -```sh -./tools/dev/run_matrix_and_smoke.sh -# or from repo root: -./hw_now.sh -``` -Cockpit shortcut: -```sh -./tools/dev/cockpit.sh rc -``` -`run_matrix_and_smoke.sh` ensures PlatformIO caches land under `$HOME/.platformio` (via `PLATFORMIO_CORE_DIR`) rather than inside the repo. -Before smoke it shows `⚠️ BRANCHE L’USB MAINTENANT ⚠️` three times, then waits for Enter while listing ports every 15s. -Each run writes deterministic artifacts under `artifacts/rc_live/_/` and logs under `logs/rc_live/_.log` (`summary.json`, `summary.md`, `ports_resolve.json`, `ui_link.log`, per-step logs). -The runner resolves macOS CP2102 by LOCATION (`20-6.1.1` ESP32, `20-6.1.2` ESP8266 USB), then enforces a dedicated `UI_LINK_STATUS connected=1` gate on ESP32. -When `ZACUS_ENV="freenove_esp32s3"` (single-board), ESP8266/UI-link/story-screen gates are marked `SKIP` with `not needed for combined board`. -Environment overrides: -- `ZACUS_REQUIRE_HW=1 ./tools/dev/run_matrix_and_smoke.sh` (fail when no hardware). -- `ZACUS_WAIT_PORT=3 ./tools/dev/run_matrix_and_smoke.sh` (override serial wait window for smoke). -- `ZACUS_NO_COUNTDOWN=1 ./tools/dev/run_matrix_and_smoke.sh` (skip the USB wait gate). -- `ZACUS_SKIP_SMOKE=1 ./tools/dev/run_matrix_and_smoke.sh` (build only, no serial smoke step). -- `ZACUS_ENV="esp32dev esp8266_oled" ./tools/dev/run_matrix_and_smoke.sh` (custom env subset). -- `ZACUS_ENV="freenove_esp32s3" ./tools/dev/run_matrix_and_smoke.sh` (single-board Freenove path). -- `ZACUS_FORCE_BUILD=1 ./tools/dev/run_matrix_and_smoke.sh` (force rebuild even when artifacts exist). -By default the smoke step exits 0 when no serial hardware is present; use `ZACUS_REQUIRE_HW=1` to enforce detection. + +## Audit & Execution Plan (2026-03-10) -## Docs +### Snapshot +- Priority: `P2` +- Tech profile: `embedded` +- Workflows: `yes` +- Tests: `yes` +- Debt markers: `0` +- Source files: `258` -- Cablage ESP32/UI: `esp32_audio/WIRING.md` -- Cablage TFT: `ui/rp2040_tft/WIRING.md` -- Quickstart flash: `docs/QUICKSTART.md` -- RC board execution: `docs/RC_FINAL_BOARD.md` -- Protocole: `protocol/ui_link_v2.md` -- Cockpit command registry: `docs/_generated/COCKPIT_COMMANDS.md` +### Corrections Prioritaires +- [ ] Vérifier target PlatformIO et budget mémoire +- [ ] Ajouter/fiabiliser les commandes de vérification automatiques. +- [ ] Clore les points bloquants avant optimisation avancée. -## Codex prompts +### Optimisation +- [ ] Identifier le hotspot principal et mesurer avant/après. +- [ ] Réduire la complexité des modules les plus touchés. -Prompt files live under `tools/dev/codex_prompts/*.prompt.md` and are designed to be consumed by the automation-friendly `codex exec` command. -Run `./tools/dev/codex_prompt_menu.sh` to see a numbered menu, pick a prompt, and send it to `codex exec --sandbox workspace-write --output-last-message artifacts/rc_live/_codex_last_message.md`. -You can also launch this helper directly from the firmware cockpit (`./tools/dev/cockpit.sh` option 6) so the existing workflow keeps a single entry point. +### Mémoire chantier +- Control plane: `/Users/electron/.codex/memories/electron_rare_chantier` +- Repo card: `/Users/electron/.codex/memories/electron_rare_chantier/REPOS/ESP32_ZACUS.md` -Story authoring prompts live separately under `docs/protocols/story_specs/prompts/*.prompt.md`. They are not ops prompts, but can still be used with Codex tooling when needed. + diff --git a/README_ESP32_ZACUS.md b/README_ESP32_ZACUS.md index dac272a..e4944e4 100644 --- a/README_ESP32_ZACUS.md +++ b/README_ESP32_ZACUS.md @@ -1,133 +1,53 @@ -# ESP32_ZACUS – Freenove ESP32-S3 UI Firmware +# ESP32_ZACUS Runtime Notes -Branche de travail dédiée pour optimisations et refactoring de l'interface Freenove. +Ce fichier résume le comportement de boot attendu sur la branche de stabilisation Freenove. -**Status**: 2026-03-01 – MVP apps/assets intégrés, build + flash OK, runtime série en reboot loop (debug en cours). +## État courant -## Intégration en place (MVP/V1 groundwork) +- Toolchain figée sur `espressif32@6.5.0`. +- Cible officielle: Freenove FNK0102H / `freenove_esp32_s3_wroom` (`N16R8`, 16 MB flash / 8 MB PSRAM). +- Le tactile reste expérimental et hors chemin release. +- L'absence de PSRAM ne lance plus le shell applicatif complet. -- Runtime apps: registre + runtime manager + modules MVP/V1 branchés. -- NES core MVP: module `nes_emulator` (validation iNES mapper 0 + actions runtime). -- ROMs NES en LittleFS: 5 homebrews dans `data/apps/nes_emulator/roms/`. -- Assets FS: icônes PNG + SFX WAV + médias fallback MP3 générés dans `data/apps/**`. -- WebUI assets: header/favicon/sfx + fontes locales. -- Fontes WebUI: `PressStart2P` + `ComicNeue` servies depuis `LittleFS` (`/webui/assets/fonts/*.ttf`). -- PlatformIO: hook pre-build pour provisionner les fontes (`scripts/pio_prepare_webui_fonts.py`). +## Signatures série attendues -## Structure - -``` -ESP32_ZACUS/ -├── ui_freenove_allinone/ # Freenove UI app (UI rendering, scenes, story integration) -├── lib/ # Story engine + auxiliary libraries -├── platformio.ini # PlatformIO config (Freenove environment) -└── README.md # General Zacus firmware guide -``` - -## Workflow - -### Setup Local - -```bash -cd ESP32_ZACUS -python3 -m venv .venv -source .venv/bin/activate -pip install platformio esptool pyserial - -# List PlatformIO boards -pio boards | grep freenove -``` - -### Build & Upload Freenove (firmware + LittleFS) - -```bash -pio run -e freenove_esp32s3_full_with_ui -pio run -e freenove_esp32s3_full_with_ui -t buildfs -pio run -e freenove_esp32s3_full_with_ui -t uploadfs --upload-port /dev/cu.usbmodem5AB90753301 -pio run -e freenove_esp32s3_full_with_ui -t upload --upload-port /dev/cu.usbmodem5AB90753301 -``` - -### Serial Monitor (115200 baud) - -```bash -pio device monitor -p /dev/cu.usbmodem5AB90753301 -b 115200 -``` - -Or Python: - -```bash -python3 << 'PY' -import serial, time -port = '/dev/cu.usbmodem5AB90753301' -ser = serial.Serial(port, 115200, timeout=0.5) -time.sleep(0.2) -ser.write(b'STATUS\n') -time.sleep(0.3) -for _ in range(10): - line = ser.readline().decode('utf-8', 'ignore').strip() - if line: - print(line) -ser.close() -PY -``` - -## Validation du 2026-03-01 - -- `build` firmware: **OK**. -- `buildfs`: **OK**. -- `uploadfs`: **OK**. -- `upload` firmware: **OK**. -- test série: **KO** (reboot loop immédiat). - -Extrait série observé: +Boot normal: ```text -rst:0x3 (RTC_SW_SYS_RST),boot:0x8 (SPI_FAST_FLASH_BOOT) -Saved PC:0x403cdb0a +[MAIN] Freenove all-in-one boot +[MEM] psram_found=1 +[APP] registry loaded=1 ... ``` -## TODO immédiat (debug runtime) +Boot diagnostic sans PSRAM: -- Capturer un backtrace exploitable (désactiver reset auto/activer logs très tôt). -- Vérifier cohérence board/flash/PSRAM par rapport au hardware réel. -- Isoler les init précoces (PSRAM, display/camera/audio) pour trouver la séquence qui déclenche `esp_restart`. -- Refaire smoke série après correctif (boot stable + init runtime + endpoints web). - -## Future Work - -- **UI Optimization** (ui_freenove_allinone): - - Refactor LVGL rendering pipeline - - Scene/story integration improvements - - Touch input handling; audio trigger feedback - -- **Story Engine** (lib/story/): - - Performance audit - - Memory footprint reduction - - ESP-NOW sync protocol optimization - -- **Hardware Integration**: - - Audio codec I2S optimization - - PSRAM buffer management - - Camera/WiFi/Bluetooth coexistence - -## Git Push to GitHub - -Once repo created on GitHub: - -```bash -cd ESP32_ZACUS -git remote add origin https://github.com/YOUR_USER/ESP32_ZACUS.git -git branch -m master main -git push -u origin main +```text +[MEM] PSRAM expected by build flags but not detected +[BOOT] safe diagnostic mode enabled: PSRAM required, app stack disabled +[SAFE] boot path: storage + serial + display + buttons only ``` -## References +## Commandes série utiles -- [Freenove Board Docs](../hardware/docs/RC_FINAL_BOARD.md) -- [Story Engine Contract](../hardware/firmware/docs/ESP_NOW_API_CONTRACT_FREENOVE_V1.md) -- [Zacus Game Protocol](../hardware/firmware/docs/ARCHITECTURE_UML.md) +Mode diagnostic: ---- +- `PING` +- `HELP` +- `STATUS` +- `BTN_READ` +- `LCD_BACKLIGHT 120` -**Last Updated**: 2026-03-01 -**Authored by**: Agent (Copilot) +Mode normal: + +- `STATUS` +- `APP_STATUS` +- `UI_MEM_STATUS` +- `NET_STATUS` +- `CAM_STATUS` + +## Debug PSRAM + +1. Lancer `./scripts/doctor_repo.sh`. +2. Flasher `freenove_esp32s3_full_with_ui`. +3. Ouvrir le monitor série à `115200`. +4. Si le mode diagnostic apparaît, vérifier le module réel, le mode flash/PSRAM et le câblage. diff --git a/boards/freenove_esp32_s3_wroom.json b/boards/freenove_esp32_s3_wroom.json new file mode 100644 index 0000000..c3e8604 --- /dev/null +++ b/boards/freenove_esp32_s3_wroom.json @@ -0,0 +1,54 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi", + "partitions": "default_8MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DARDUINO_FREENOVE_ESP32_S3_WROOM", + "-DARDUINO_USB_MODE=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1", + "-DBOARD_HAS_PSRAM" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [ + [ + "0x303A", + "0x1001" + ] + ], + "mcu": "esp32s3", + "psram_type": "opi", + "variant": "esp32s3" + }, + "connectivity": [ + "bluetooth", + "wifi" + ], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": [ + "esp-builtin" + ], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "Freenove ESP32-S3 WROOM N16R8 (16MB Flash / 8MB PSRAM)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://github.com/Freenove/Freenove_ESP32_S3_WROOM_Board", + "vendor": "Freenove" +} diff --git a/docs/FNK0102H_SOURCE_OF_TRUTH.md b/docs/FNK0102H_SOURCE_OF_TRUTH.md new file mode 100644 index 0000000..51d14a0 --- /dev/null +++ b/docs/FNK0102H_SOURCE_OF_TRUTH.md @@ -0,0 +1,73 @@ +# FNK0102H Source Of Truth + +## Cible officielle + +| Élément | Valeur | +| --- | --- | +| Carte | Freenove FNK0102H | +| Module / board profile | Freenove ESP32-S3 WROOM N16R8 | +| Flash | 16 MB | +| PSRAM | 8 MB | +| Framework | Arduino | +| PlatformIO | `espressif32@6.5.0` | +| Env canonique | `freenove_esp32s3_full_with_ui` | +| `board` PlatformIO | `freenove_esp32_s3_wroom` | +| Board manifest local | `boards/freenove_esp32_s3_wroom.json` | + +## Mémoire et build + +| Paramètre | Valeur | +| --- | --- | +| `board_build.flash_size` | `16MB` | +| `board_upload.maximum_size` | `4194304` | +| `board_build.partitions` | `partitions/freenove_esp32s3_app4mb_fs12096kb.csv` | +| `FREENOVE_HAS_TOUCH` | `0` en release | + +## Écran et entrées + +| Fonction | GPIO / Paramètre | +| --- | --- | +| LCD variant | `FNK0102H_ST7796_320x480` | +| Rotation | `1` | +| TFT SCK | 47 | +| TFT MOSI | 21 | +| TFT DC | 45 | +| TFT RESET | 20 | +| TFT BL | 2 | +| Boutons 5 directions | GPIO19 via ladder analogique | + +## Stockage et audio + +| Fonction | GPIO | +| --- | --- | +| SD CMD | 38 | +| SD CLK | 39 | +| SD D0 | 40 | +| I2S WS | 41 | +| I2S BCK | 42 | +| I2S DOUT | 1 | +| I2S IN SCK / WS / DIN | 3 / 14 / 46 | + +## Caméra + +| Fonction | GPIO | +| --- | --- | +| XCLK | 15 | +| SIOD / SIOC | 4 / 5 | +| Y2 / Y3 / Y4 / Y5 / Y6 | 11 / 9 / 8 / 10 / 12 | +| Y7 / Y8 / Y9 | 18 / 17 / 16 | +| VSYNC / HREF / PCLK | 6 / 7 / 13 | +| PWDN / RESET | `-1` / `-1` | + +## Hors support release + +- `freenove_esp32s3_touch` est expérimental. +- Le tactile n'est pas une interface officielle du chemin release. +- Les broches `GPIO9` et `GPIO15` sont déjà utilisées par la caméra dans ce profil. +- Toute tentative d'activer le tactile doit être traitée comme une variante séparée, hors CI et hors release. + +## Sources externes + +- +- +- diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md new file mode 100644 index 0000000..eb3e6a4 --- /dev/null +++ b/docs/QUICKSTART.md @@ -0,0 +1,70 @@ +# Quickstart + +## Prérequis + +- `python3` +- accès USB vers la carte Freenove +- réseau local optionnel pour les tests HTTP + +## Bootstrap + +```bash +./scripts/bootstrap_platformio.sh +./scripts/doctor_repo.sh +source .venv/bin/activate +``` + +Le bootstrap crée une `.venv` locale et installe `platformio`, `esptool` et `pyserial`. + +## Build + +```bash +python -m platformio run -e freenove_esp32s3_full_with_ui +python -m platformio run -e freenove_esp32s3_full_with_ui -t buildfs +``` + +## Flash + +```bash +python -m platformio run -e freenove_esp32s3_full_with_ui -t uploadfs --upload-port +python -m platformio run -e freenove_esp32s3_full_with_ui -t upload --upload-port +``` + +## Monitor + +```bash +python -m platformio device monitor -b 115200 --port +``` + +## Signatures attendues + +Boot normal: + +```text +[MAIN] Freenove all-in-one boot +[MEM] psram_found=1 +``` + +Boot diagnostic sans PSRAM: + +```text +[MEM] PSRAM expected by build flags but not detected +[BOOT] safe diagnostic mode enabled: PSRAM required, app stack disabled +[SAFE] boot path: storage + serial + display + buttons only +``` + +## Smoke minimal + +```bash +python tests/sprint1_utility_contract.py --mode serial --port +python tests/sprint2_capture_contract.py --mode serial --port +python tests/sprint3_audio_contract.py --mode serial --port +python tests/phase9_ui_validation.py --port +``` + +## Procédure si la PSRAM est absente + +1. Garder le monitor série ouvert. +2. Vérifier que le boot reste vivant en mode diagnostic. +3. Confirmer l'absence d'initialisation Wi-Fi, ESP-NOW, caméra et audio. +4. Vérifier le module réel et la config `platformio.ini` avant de reflasher. diff --git a/lib/ESP32QRCodeReader_vendor/include/ESP32CameraPins.h b/lib/ESP32QRCodeReader_vendor/include/ESP32CameraPins.h new file mode 100644 index 0000000..0b565c1 --- /dev/null +++ b/lib/ESP32QRCodeReader_vendor/include/ESP32CameraPins.h @@ -0,0 +1,184 @@ +#ifndef ESP32_CAMERA_PINS_H_ +#define ESP32_CAMERA_PINS_H_ + +struct CameraPins +{ + int PWDN_GPIO_NUM; + int RESET_GPIO_NUM; + int XCLK_GPIO_NUM; + int SIOD_GPIO_NUM; + int SIOC_GPIO_NUM; + int Y9_GPIO_NUM; + int Y8_GPIO_NUM; + int Y7_GPIO_NUM; + int Y6_GPIO_NUM; + int Y5_GPIO_NUM; + int Y4_GPIO_NUM; + int Y3_GPIO_NUM; + int Y2_GPIO_NUM; + int VSYNC_GPIO_NUM; + int HREF_GPIO_NUM; + int PCLK_GPIO_NUM; +}; + +#define CAMERA_MODEL_WROVER_KIT \ + { \ + .PWDN_GPIO_NUM = -1, \ + .RESET_GPIO_NUM = -1, \ + .XCLK_GPIO_NUM = 21, \ + .SIOD_GPIO_NUM = 26, \ + .SIOC_GPIO_NUM = 27, \ + .Y9_GPIO_NUM = 35, \ + .Y8_GPIO_NUM = 34, \ + .Y7_GPIO_NUM = 39, \ + .Y6_GPIO_NUM = 36, \ + .Y5_GPIO_NUM = 19, \ + .Y4_GPIO_NUM = 18, \ + .Y3_GPIO_NUM = 5, \ + .Y2_GPIO_NUM = 4, \ + .VSYNC_GPIO_NUM = 25, \ + .HREF_GPIO_NUM = 23, \ + .PCLK_GPIO_NUM = 22, \ + } + +#define CAMERA_MODEL_ESP_EYE \ + { \ + .PWDN_GPIO_NUM = -1, \ + .RESET_GPIO_NUM = -1, \ + .XCLK_GPIO_NUM = 4, \ + .SIOD_GPIO_NUM = 18, \ + .SIOC_GPIO_NUM = 23, \ + .Y9_GPIO_NUM = 36, \ + .Y8_GPIO_NUM = 37, \ + .Y7_GPIO_NUM = 38, \ + .Y6_GPIO_NUM = 39, \ + .Y5_GPIO_NUM = 35, \ + .Y4_GPIO_NUM = 14, \ + .Y3_GPIO_NUM = 13, \ + .Y2_GPIO_NUM = 34, \ + .VSYNC_GPIO_NUM = 5, \ + .HREF_GPIO_NUM = 27, \ + .PCLK_GPIO_NUM = 25, \ + } + +#define CAMERA_MODEL_M5STACK_PSRAM \ + { \ + .PWDN_GPIO_NUM = -1, \ + .RESET_GPIO_NUM = 15, \ + .XCLK_GPIO_NUM = 27, \ + .SIOD_GPIO_NUM = 25, \ + .SIOC_GPIO_NUM = 23, \ + .Y9_GPIO_NUM = 19, \ + .Y8_GPIO_NUM = 36, \ + .Y7_GPIO_NUM = 18, \ + .Y6_GPIO_NUM = 39, \ + .Y5_GPIO_NUM = 5, \ + .Y4_GPIO_NUM = 34, \ + .Y3_GPIO_NUM = 35, \ + .Y2_GPIO_NUM = 32, \ + .VSYNC_GPIO_NUM = 22, \ + .HREF_GPIO_NUM = 26, \ + .PCLK_GPIO_NUM = 21, \ + } + +#define CAMERA_MODEL_M5STACK_V2_PSRAM \ + { \ + .PWDN_GPIO_NUM = -1, \ + .RESET_GPIO_NUM = 15, \ + .XCLK_GPIO_NUM = 27, \ + .SIOD_GPIO_NUM = 22, \ + .SIOC_GPIO_NUM = 23, \ + .Y9_GPIO_NUM = 19, \ + .Y8_GPIO_NUM = 36, \ + .Y7_GPIO_NUM = 18, \ + .Y6_GPIO_NUM = 39, \ + .Y5_GPIO_NUM = 5, \ + .Y4_GPIO_NUM = 34, \ + .Y3_GPIO_NUM = 35, \ + .Y2_GPIO_NUM = 32, \ + .VSYNC_GPIO_NUM = 25, \ + .HREF_GPIO_NUM = 26, \ + .PCLK_GPIO_NUM = 21, \ + } + +#define CAMERA_MODEL_M5STACK_WIDE \ + { \ + .PWDN_GPIO_NUM = -1, \ + .RESET_GPIO_NUM = 15, \ + .XCLK_GPIO_NUM = 27, \ + .SIOD_GPIO_NUM = 22, \ + .SIOC_GPIO_NUM = 23, \ + .Y9_GPIO_NUM = 19, \ + .Y8_GPIO_NUM = 36, \ + .Y7_GPIO_NUM = 18, \ + .Y6_GPIO_NUM = 39, \ + .Y5_GPIO_NUM = 5, \ + .Y4_GPIO_NUM = 34, \ + .Y3_GPIO_NUM = 35, \ + .Y2_GPIO_NUM = 32, \ + .VSYNC_GPIO_NUM = 25, \ + .HREF_GPIO_NUM = 26, \ + .PCLK_GPIO_NUM = 21, \ + } + +#define CAMERA_MODEL_M5STACK_ESP32CAM \ + { \ + .PWDN_GPIO_NUM = -1, \ + .RESET_GPIO_NUM = 15, \ + .XCLK_GPIO_NUM = 27, \ + .SIOD_GPIO_NUM = 25, \ + .SIOC_GPIO_NUM = 23, \ + .Y9_GPIO_NUM = 19, \ + .Y8_GPIO_NUM = 36, \ + .Y7_GPIO_NUM = 18, \ + .Y6_GPIO_NUM = 39, \ + .Y5_GPIO_NUM = 5, \ + .Y4_GPIO_NUM = 34, \ + .Y3_GPIO_NUM = 35, \ + .Y2_GPIO_NUM = 17, \ + .VSYNC_GPIO_NUM = 22, \ + .HREF_GPIO_NUM = 26, \ + .PCLK_GPIO_NUM = 21, \ + } + +#define CAMERA_MODEL_AI_THINKER \ + { \ + .PWDN_GPIO_NUM = 32, \ + .RESET_GPIO_NUM = -1, \ + .XCLK_GPIO_NUM = 0, \ + .SIOD_GPIO_NUM = 26, \ + .SIOC_GPIO_NUM = 27, \ + .Y9_GPIO_NUM = 35, \ + .Y8_GPIO_NUM = 34, \ + .Y7_GPIO_NUM = 39, \ + .Y6_GPIO_NUM = 36, \ + .Y5_GPIO_NUM = 21, \ + .Y4_GPIO_NUM = 19, \ + .Y3_GPIO_NUM = 18, \ + .Y2_GPIO_NUM = 5, \ + .VSYNC_GPIO_NUM = 25, \ + .HREF_GPIO_NUM = 23, \ + .PCLK_GPIO_NUM = 22, \ + } + +#define CAMERA_MODEL_TTGO_T_JOURNAL \ + { \ + .PWDN_GPIO_NUM = 0, \ + .RESET_GPIO_NUM = 15, \ + .XCLK_GPIO_NUM = 27, \ + .SIOD_GPIO_NUM = 25, \ + .SIOC_GPIO_NUM = 23, \ + .Y9_GPIO_NUM = 19, \ + .Y8_GPIO_NUM = 36, \ + .Y7_GPIO_NUM = 18, \ + .Y6_GPIO_NUM = 39, \ + .Y5_GPIO_NUM = 5, \ + .Y4_GPIO_NUM = 34, \ + .Y3_GPIO_NUM = 35, \ + .Y2_GPIO_NUM = 17, \ + .VSYNC_GPIO_NUM = 22, \ + .HREF_GPIO_NUM = 26, \ + .PCLK_GPIO_NUM = 21, \ + } + +#endif //ESP32_CAMERA_PINS_H_ \ No newline at end of file diff --git a/lib/ESP32QRCodeReader_vendor/include/ESP32QRCodeReader.h b/lib/ESP32QRCodeReader_vendor/include/ESP32QRCodeReader.h new file mode 100644 index 0000000..68b0ab4 --- /dev/null +++ b/lib/ESP32QRCodeReader_vendor/include/ESP32QRCodeReader.h @@ -0,0 +1,63 @@ +#ifndef ESP32_QR_CODE_ARDUINO_H_ +#define ESP32_QR_CODE_ARDUINO_H_ + +#include "Arduino.h" +#include "ESP32CameraPins.h" +#include "esp_camera.h" + +#ifndef QR_CODE_READER_STACK_SIZE +#define QR_CODE_READER_STACK_SIZE 40 * 1024 +#endif + +#ifndef QR_CODE_READER_TASK_PRIORITY +#define QR_CODE_READER_TASK_PRIORITY 5 +#endif + +enum QRCodeReaderSetupErr +{ + SETUP_OK, + SETUP_NO_PSRAM_ERROR, + SETUP_CAMERA_INIT_ERROR, +}; + +/* This structure holds the decoded QR-code data */ +struct QRCodeData +{ + bool valid; + int dataType; + uint8_t payload[1024]; + int payloadLen; +}; + +class ESP32QRCodeReader +{ +private: + TaskHandle_t qrCodeTaskHandler; + CameraPins pins; + framesize_t frameSize; + +public: + camera_config_t cameraConfig; + QueueHandle_t qrCodeQueue; + bool begun = false; + bool debug = false; + + // Constructor + ESP32QRCodeReader(); + ESP32QRCodeReader(CameraPins pins); + ESP32QRCodeReader(CameraPins pins, framesize_t frameSize); + ESP32QRCodeReader(framesize_t frameSize); + ~ESP32QRCodeReader(); + + // Setup camera + QRCodeReaderSetupErr setup(); + + void begin(); + void beginOnCore(BaseType_t core); + bool receiveQrCode(struct QRCodeData *qrCodeData, long timeoutMs); + void end(); + + void setDebug(bool); +}; + +#endif // ESP32_QR_CODE_ARDUINO_H_ \ No newline at end of file diff --git a/lib/ESP32QRCodeReader_vendor/library.json b/lib/ESP32QRCodeReader_vendor/library.json new file mode 100644 index 0000000..83ef397 --- /dev/null +++ b/lib/ESP32QRCodeReader_vendor/library.json @@ -0,0 +1,22 @@ +{ + "name": "ESP32QRCodeReader", + "version": "1.1.0", + "description": "A library to read QR Codes using an ESP32 with a camera module", + "keywords": "qrcode, esp32, camera", + "repository": { + "type": "git", + "url": "https://github.com/alvarowolfx/ESP32QRCodeReader" + }, + "authors": [ + { + "name": "Alvaro Viebrantz", + "email": "alvarowolfx@gmail.com", + "url": "https://github.com/alvarowolfx", + "maintainer": true + } + ], + "license": "MIT", + "homepage": "https://github.com/alvarowolfx/ESP32QRCodeReader", + "frameworks": "arduino", + "platforms": "espressif32" +} diff --git a/lib/ESP32QRCodeReader_vendor/library.properties b/lib/ESP32QRCodeReader_vendor/library.properties new file mode 100644 index 0000000..05d0029 --- /dev/null +++ b/lib/ESP32QRCodeReader_vendor/library.properties @@ -0,0 +1,9 @@ +name=ESP32QRCodeReader +version=1.1.0 +author=Alvaro Viebrantz +maintainer=Alvaro Viebrantz +sentence=A library to read QR Codes using an ESP32 with a camera module. +paragraph=A library to read QR Codes using an ESP32 with a camera module +category=Data Processing +url=https://github.com/alvarowolfx/ESP32QRCodeReader +architectures=* \ No newline at end of file diff --git a/lib/ESP32QRCodeReader_vendor/src/ESP32QRCodeReader.cpp b/lib/ESP32QRCodeReader_vendor/src/ESP32QRCodeReader.cpp new file mode 100644 index 0000000..12dfb5f --- /dev/null +++ b/lib/ESP32QRCodeReader_vendor/src/ESP32QRCodeReader.cpp @@ -0,0 +1,290 @@ +#include "ESP32QRCodeReader.h" + +#include "quirc/quirc.h" +#include "Arduino.h" + +ESP32QRCodeReader::ESP32QRCodeReader() : ESP32QRCodeReader(CAMERA_MODEL_AI_THINKER, FRAMESIZE_QVGA) +{ +} + +ESP32QRCodeReader::ESP32QRCodeReader(framesize_t frameSize) : ESP32QRCodeReader(CAMERA_MODEL_AI_THINKER, frameSize) +{ +} + +ESP32QRCodeReader::ESP32QRCodeReader(CameraPins pins) : ESP32QRCodeReader(pins, FRAMESIZE_QVGA) +{ +} + +ESP32QRCodeReader::ESP32QRCodeReader(CameraPins pins, framesize_t frameSize) : pins(pins), frameSize(frameSize) +{ + qrCodeQueue = xQueueCreate(10, sizeof(struct QRCodeData)); +} + +ESP32QRCodeReader::~ESP32QRCodeReader() +{ + end(); +} + +QRCodeReaderSetupErr ESP32QRCodeReader::setup() +{ + if (!psramFound()) + { + return SETUP_NO_PSRAM_ERROR; + } + + cameraConfig.ledc_channel = LEDC_CHANNEL_0; + cameraConfig.ledc_timer = LEDC_TIMER_0; + cameraConfig.pin_d0 = pins.Y2_GPIO_NUM; + cameraConfig.pin_d1 = pins.Y3_GPIO_NUM; + cameraConfig.pin_d2 = pins.Y4_GPIO_NUM; + cameraConfig.pin_d3 = pins.Y5_GPIO_NUM; + cameraConfig.pin_d4 = pins.Y6_GPIO_NUM; + cameraConfig.pin_d5 = pins.Y7_GPIO_NUM; + cameraConfig.pin_d6 = pins.Y8_GPIO_NUM; + cameraConfig.pin_d7 = pins.Y9_GPIO_NUM; + cameraConfig.pin_xclk = pins.XCLK_GPIO_NUM; + cameraConfig.pin_pclk = pins.PCLK_GPIO_NUM; + cameraConfig.pin_vsync = pins.VSYNC_GPIO_NUM; + cameraConfig.pin_href = pins.HREF_GPIO_NUM; + cameraConfig.pin_sscb_sda = pins.SIOD_GPIO_NUM; + cameraConfig.pin_sscb_scl = pins.SIOC_GPIO_NUM; + cameraConfig.pin_pwdn = pins.PWDN_GPIO_NUM; + cameraConfig.pin_reset = pins.RESET_GPIO_NUM; + cameraConfig.xclk_freq_hz = 10000000; + cameraConfig.pixel_format = PIXFORMAT_GRAYSCALE; + + //cameraConfig.frame_size = FRAMESIZE_VGA; + cameraConfig.frame_size = frameSize; + cameraConfig.jpeg_quality = 15; + cameraConfig.fb_count = 1; + +#if defined(CAMERA_MODEL_ESP_EYE) + pinMode(13, INPUT_PULLUP); + pinMode(14, INPUT_PULLUP); +#endif + + // camera init + esp_err_t err = esp_camera_init(&cameraConfig); + if (err != ESP_OK) + { + return SETUP_CAMERA_INIT_ERROR; + } + return SETUP_OK; +} + +void dumpData(const struct quirc_data *data) +{ + Serial.printf("Version: %d\n", data->version); + Serial.printf("ECC level: %c\n", "MLHQ"[data->ecc_level]); + Serial.printf("Mask: %d\n", data->mask); + Serial.printf("Length: %d\n", data->payload_len); + Serial.printf("Payload: %s\n", data->payload); +} + +void qrCodeDetectTask(void *taskData) +{ + ESP32QRCodeReader *self = (ESP32QRCodeReader *)taskData; + camera_config_t camera_config = self->cameraConfig; + if (camera_config.frame_size > FRAMESIZE_SVGA) + { + if (self->debug) + { + Serial.println("Camera Size err"); + } + vTaskDelete(NULL); + return; + } + + struct quirc *q = NULL; + uint8_t *image = NULL; + camera_fb_t *fb = NULL; + + uint16_t old_width = 0; + uint16_t old_height = 0; + + if (self->debug) + { + Serial.printf("begin to qr_recoginze\r\n"); + } + q = quirc_new(); + if (q == NULL) + { + if (self->debug) + { + Serial.print("can't create quirc object\r\n"); + } + vTaskDelete(NULL); + return; + } + + while (true) + { + + if (self->debug) + { + Serial.printf("alloc qr heap: %u\r\n", xPortGetFreeHeapSize()); + Serial.printf("uxHighWaterMark = %d\r\n", uxTaskGetStackHighWaterMark(NULL)); + Serial.print("begin camera get fb\r\n"); + } + vTaskDelay(100 / portTICK_PERIOD_MS); + + fb = esp_camera_fb_get(); + if (!fb) + { + if (self->debug) + { + Serial.println("Camera capture failed"); + } + continue; + } + + if (old_width != fb->width || old_height != fb->height) + { + if (self->debug) + { + Serial.printf("Recognizer size change w h len: %d, %d, %d \r\n", fb->width, fb->height, fb->len); + Serial.println("Resize the QR-code recognizer."); + // Resize the QR-code recognizer. + } + if (quirc_resize(q, fb->width, fb->height) < 0) + { + if (self->debug) + { + Serial.println("Resize the QR-code recognizer err (cannot allocate memory)."); + } + esp_camera_fb_return(fb); + fb = NULL; + image = NULL; + continue; + } + else + { + old_width = fb->width; + old_height = fb->height; + } + } + + // Serial.printf("quirc_begin\r\n"); + image = quirc_begin(q, NULL, NULL); + if (self->debug) + { + Serial.printf("Frame w h len: %d, %d, %d \r\n", fb->width, fb->height, fb->len); + } + memcpy(image, fb->buf, fb->len); + quirc_end(q); + + if (self->debug) + { + Serial.printf("quirc_end\r\n"); + } + int count = quirc_count(q); + if (count == 0) + { + if (self->debug) + { + Serial.printf("Error: not a valid qrcode\n"); + } + esp_camera_fb_return(fb); + fb = NULL; + image = NULL; + continue; + } + + for (int i = 0; i < count; i++) + { + struct quirc_code code; + struct quirc_data data; + quirc_decode_error_t err; + + quirc_extract(q, i, &code); + err = quirc_decode(&code, &data); + + struct QRCodeData qrCodeData; + + if (err) + { + const char *error = quirc_strerror(err); + int len = strlen(error); + if (self->debug) + { + Serial.printf("Decoding FAILED: %s\n", error); + } + for (int i = 0; i < len; i++) + { + qrCodeData.payload[i] = error[i]; + } + qrCodeData.valid = false; + qrCodeData.payload[len] = '\0'; + qrCodeData.payloadLen = len; + } + else + { + if (self->debug) + { + Serial.printf("Decoding successful:\n"); + dumpData(&data); + } + + qrCodeData.dataType = data.data_type; + for (int i = 0; i < data.payload_len; i++) + { + qrCodeData.payload[i] = data.payload[i]; + } + qrCodeData.valid = true; + qrCodeData.payload[data.payload_len] = '\0'; + qrCodeData.payloadLen = data.payload_len; + } + xQueueSend(self->qrCodeQueue, &qrCodeData, (TickType_t)0); + + if (self->debug) + { + Serial.println(); + } + } + + //Serial.printf("finish recoginize\r\n"); + esp_camera_fb_return(fb); + fb = NULL; + image = NULL; + } + quirc_destroy(q); + vTaskDelete(NULL); +} + +void ESP32QRCodeReader::begin() +{ + beginOnCore(0); +} + +void ESP32QRCodeReader::beginOnCore(BaseType_t core) +{ + if (!begun) + { + xTaskCreatePinnedToCore(qrCodeDetectTask, "qrCodeDetectTask", QR_CODE_READER_STACK_SIZE, this, QR_CODE_READER_TASK_PRIORITY, &qrCodeTaskHandler, core); + begun = true; + } +} + +bool ESP32QRCodeReader::receiveQrCode(struct QRCodeData *qrCodeData, long timeoutMs) +{ + return xQueueReceive(qrCodeQueue, qrCodeData, (TickType_t)pdMS_TO_TICKS(timeoutMs)) != 0; +} + +void ESP32QRCodeReader::end() +{ + if (begun) + { + TaskHandle_t tmpTask = qrCodeTaskHandler; + if (qrCodeTaskHandler != NULL) + { + qrCodeTaskHandler = NULL; + vTaskDelete(tmpTask); + } + } + begun = false; +} + +void ESP32QRCodeReader::setDebug(bool on) +{ + debug = on; +} diff --git a/lib/ESP32QRCodeReader_vendor/src/openmv/collections.c b/lib/ESP32QRCodeReader_vendor/src/openmv/collections.c new file mode 100644 index 0000000..ea0073d --- /dev/null +++ b/lib/ESP32QRCodeReader_vendor/src/openmv/collections.c @@ -0,0 +1,85 @@ +/* This file is part of the OpenMV project. + * Copyright (c) 2013-2017 Ibrahim Abdelkader & Kwabena W. Agyeman + * This work is licensed under the MIT license, see the file LICENSE for details. + */ + +#include "collections.h" +#define CHAR_BITS (sizeof(char) * 8) +#define CHAR_MASK (CHAR_BITS - 1) +#define CHAR_SHIFT IM_LOG2(CHAR_MASK) + +////////// +// lifo // +////////// + +void lifo_alloc(lifo_t *ptr, size_t size, size_t data_len) +{ + ptr->len = 0; + ptr->size = size; + ptr->data_len = data_len; + ptr->data = (char *)ps_malloc(size * data_len); +} + +void lifo_alloc_all(lifo_t *ptr, size_t *size, size_t data_len) +{ + ptr->data = (char *)ps_malloc(255); + ptr->data_len = data_len; + ptr->size = 255 / data_len; + ptr->len = 0; + *size = ptr->size; +} + +void lifo_free(lifo_t *ptr) +{ + if (ptr->data) + { + free(ptr->data); + } +} + +void lifo_clear(lifo_t *ptr) +{ + ptr->len = 0; +} + +size_t lifo_size(lifo_t *ptr) +{ + return ptr->len; +} + +bool lifo_is_not_empty(lifo_t *ptr) +{ + return ptr->len; +} + +bool lifo_is_not_full(lifo_t *ptr) +{ + return ptr->len != ptr->size; +} + +void lifo_enqueue(lifo_t *ptr, void *data) +{ + memcpy(ptr->data + (ptr->len * ptr->data_len), data, ptr->data_len); + + ptr->len += 1; +} + +void lifo_dequeue(lifo_t *ptr, void *data) +{ + if (data) + { + memcpy(data, ptr->data + ((ptr->len - 1) * ptr->data_len), ptr->data_len); + } + + ptr->len -= 1; +} + +void lifo_poke(lifo_t *ptr, void *data) +{ + memcpy(ptr->data + (ptr->len * ptr->data_len), data, ptr->data_len); +} + +void lifo_peek(lifo_t *ptr, void *data) +{ + memcpy(data, ptr->data + ((ptr->len - 1) * ptr->data_len), ptr->data_len); +} diff --git a/lib/ESP32QRCodeReader_vendor/src/openmv/collections.h b/lib/ESP32QRCodeReader_vendor/src/openmv/collections.h new file mode 100644 index 0000000..711cafa --- /dev/null +++ b/lib/ESP32QRCodeReader_vendor/src/openmv/collections.h @@ -0,0 +1,33 @@ +/* This file is part of the OpenMV project. + * Copyright (c) 2013-2017 Ibrahim Abdelkader & Kwabena W. Agyeman + * This work is licensed under the MIT license, see the file LICENSE for details. + */ + +#ifndef __COLLECTIONS_H__ +#define __COLLECTIONS_H__ +#include +#include +////////// +// lifo // +////////// + +typedef struct lifo +{ + size_t len, size, data_len; + char *data; +} +__attribute__((aligned(8))) lifo_t; + +void lifo_alloc(lifo_t *ptr, size_t size, size_t data_len); +void lifo_alloc_all(lifo_t *ptr, size_t *size, size_t data_len); +void lifo_free(lifo_t *ptr); +void lifo_clear(lifo_t *ptr); +size_t lifo_size(lifo_t *ptr); +bool lifo_is_not_empty(lifo_t *ptr); +bool lifo_is_not_full(lifo_t *ptr); +void lifo_enqueue(lifo_t *ptr, void *data); +void lifo_dequeue(lifo_t *ptr, void *data); +void lifo_poke(lifo_t *ptr, void *data); +void lifo_peek(lifo_t *ptr, void *data); + +#endif /* __COLLECTIONS_H__ */ \ No newline at end of file diff --git a/lib/ESP32QRCodeReader_vendor/src/openmv/fmath.h b/lib/ESP32QRCodeReader_vendor/src/openmv/fmath.h new file mode 100644 index 0000000..76c3b93 --- /dev/null +++ b/lib/ESP32QRCodeReader_vendor/src/openmv/fmath.h @@ -0,0 +1,70 @@ +/* + * This file is part of the OpenMV project. + * Copyright (c) 2013/2014 Ibrahim Abdelkader + * This work is licensed under the MIT license, see the file LICENSE for details. + * + * Fast approximate math functions. + * + */ +#ifndef __FMATH_H +#define __FMATH_H +#include +#include + +static inline float fast_sqrtf(float x) +{ + //return sqrtf(x); + asm("fsqrt.s %0, %1" + : "=f"(x) + : "f"(x)); + return x; +} +static inline int fast_floorf(float x) +{ + return (int)(x); +} + +static inline int fast_ceilf(float x) +{ + return (int)(x + 0.9999f); +} + +static inline int fast_roundf(float x) +{ + return (int)(x); +} + +static inline float fast_fabsf(float d) +{ + return fabsf(d); +} + +extern int fast_floorf(float x); +extern int fast_ceilf(float x); +extern int fast_roundf(float x); +extern float fast_atanf(float x); +extern float fast_atan2f(float y, float x); +extern float fast_expf(float x); +extern float fast_cbrtf(float d); +extern float fast_fabsf(float d); +extern float fast_log(float x); +extern float fast_log2(float x); +extern float fast_powf(float a, float b); + +/*#define fast_sqrtf(x) (sqrtf(x)) +#define fast_floorf(x) ((int)floorf(x)) +#define fast_ceilf(x) ((int)ceilf(x)) +#define fast_roundf(x) ((int)roundf(x)) +#define fast_atanf(x) (atanf(x)) +#define fast_atan2f(x,y) (atan2f((x),(y))) +#define fast_expf(x) (expf(x)) +#define fast_cbrtf(x) (cbrtf(x)) +#define fast_fabsf(x) (fabsf(x)) +#define fast_log(x) (log(x)) +#define fast_log2(x) (log2(x)) +#define fast_powf(x,y) (powf((x),(y))) +*/ + +extern const float cos_table[360]; +extern const float sin_table[360]; +#endif // __FMATH_H \ No newline at end of file diff --git a/lib/ESP32QRCodeReader_vendor/src/quirc/LICENSE b/lib/ESP32QRCodeReader_vendor/src/quirc/LICENSE new file mode 100644 index 0000000..d47c026 --- /dev/null +++ b/lib/ESP32QRCodeReader_vendor/src/quirc/LICENSE @@ -0,0 +1,16 @@ +quirc -- QR-code recognition library +Copyright (C) 2010-2012 Daniel Beer + +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted, provided that the +above copyright notice and this permission notice appear in all +copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR +PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/lib/ESP32QRCodeReader_vendor/src/quirc/README.md b/lib/ESP32QRCodeReader_vendor/src/quirc/README.md new file mode 100644 index 0000000..d70c5c3 --- /dev/null +++ b/lib/ESP32QRCodeReader_vendor/src/quirc/README.md @@ -0,0 +1,193 @@ +Quirc +===== + +QR codes are a type of high-density matrix barcodes, and quirc is a library for +extracting and decoding them from images. It has several features which make it +a good choice for this purpose: + +* It is fast enough to be used with realtime video: extracting and decoding + from VGA frame takes about 50 ms on a modern x86 core. + +* It has a robust and tolerant recognition algorithm. It can correctly + recognise and decode QR codes which are rotated and/or oblique to the camera. + It can also distinguish and decode multiple codes within the same image. + +* It is easy to use, with a simple API described in a single commented header + file (see below for an overview). + +* It is small and easily embeddable, with no dependencies other than standard C + functions. + +* It has a very small memory footprint: one byte per image pixel, plus a few kB + per decoder object. + +* It uses no global mutable state, and is safe to use in a multithreaded + application. + +* BSD-licensed, with almost no restrictions regarding use and/or modification. + +The distribution comes with, in addition to the library, several test programs. +While the core library is very portable, these programs have some additional +dependencies. All of them require libjpeg, and two (`quirc-demo` and `inspect`) +require SDL. The camera demos use Linux-specific APIs: + +### quirc-demo + +This is an real-time demo which requires a camera and a graphical display. The +video stream is displayed on screen as it's received, and any QR codes +recognised are highlighted in the image, with the decoded information both +displayed on the image and printed on stdout. + +### quirc-scanner + +This program turns your camera into a barcode scanner. It's almost the same as +the `demo` application, but it doesn't display the video stream, and thus +doesn't require a graphical display. + +### qrtest + +This test is used to evaluate the performance of library. Given a directory +tree containing a bunch of JPEG images, it will attempt to locate and decode QR +codes in each image. Speed and success statistics are collected and printed on +stdout. + +### inspect + +This test is used for debugging. Given a single JPEG image, it will display a +diagram showing the internal state of the decoder as well as printing +additional information on stdout. + +Installation +------------ +To build the library and associated demos/tests, type `make`. If you need to +decode "large" image files build with `CFLAGS="-DQUIRC_MAX_REGIONS=65534" make` +instead. Note that this will increase the memory usage, it is discouraged for +low resource devices (i.e. embedded). + +Type `make install` to install the library, header file and camera demos. + +You can specify one or several of the following targets if you don't want, or +are unable to build everything: + +* libquirc.a +* libquirc.so +* qrtest +* inspect +* quirc-scanner +* quirc-demo + +Library use +----------- +All of the library's functionality is exposed through a single header file, +which you should include: + +```C +#include +``` + +To decode images, you'll need to instantiate a `struct quirc` object, which is +done with the `quirc_new` function. Later, when you no longer need to decode +anything, you should release the allocated memory with `quirc_destroy`: + +```C +struct quirc *qr; + +qr = quirc_new(); +if (!qr) { + perror("Failed to allocate memory"); + abort(); +} + +/* ... */ + +quirc_destroy(qr); +``` + +Having obtained a decoder object, you need to set the image size that you'll be +working with, which is done using `quirc_resize`: + +```C +if (quirc_resize(qr, 640, 480) < 0) { + perror("Failed to allocate video memory"); + abort(); +} +``` + +`quirc_resize` and `quirc_new` are the only library functions which allocate +memory. If you plan to process a series of frames (or a video stream), you +probably want to allocate and size a single decoder and hold onto it to process +each frame. + +Processing frames is done in two stages. The first stage is an +image-recognition stage called identification, which takes a grayscale image +and searches for QR codes. Using `quirc_begin` and `quirc_end`, you can feed a +grayscale image directly into the buffer that `quirc` uses for image +processing: + +```C +uint8_t *image; +int w, h; + +image = quirc_begin(qr, &w, &h); + +/* Fill out the image buffer here. + * image is a pointer to a w*h bytes. + * One byte per pixel, w pixels per line, h lines in the buffer. + */ + +quirc_end(qr); +``` + +Note that `quirc_begin` simply returns a pointer to a previously allocated +buffer. The buffer will contain uninitialized data. After the call to +`quirc_end`, the decoder holds a list of detected QR codes which can be queried +via `quirc_count` and `quirc_extract`. + +At this point, the second stage of processing occurs -- decoding. This is done +via the call to `quirc_decode`, which is not associated with a decoder object. + +```C +int num_codes; +int i; + +/* We've previously fed an image to the decoder via +* quirc_begin/quirc_end. +*/ + +num_codes = quirc_count(qr); +for (i = 0; i < num_codes; i++) { + struct quirc_code code; + struct quirc_data data; + quirc_decode_error_t err; + + quirc_extract(qr, i, &code); + + /* Decoding stage */ + err = quirc_decode(&code, &data); + if (err) + printf("DECODE FAILED: %s\n", quirc_strerror(err)); + else + printf("Data: %s\n", data.payload); +} +``` + +`quirc_code` and `quirc_data` are flat structures which don't need to be +initialized or freed after use. + +Copyright +--------- +Copyright (C) 2010-2012 Daniel Beer <> + +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted, provided that the +above copyright notice and this permission notice appear in all +copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR +PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/lib/ESP32QRCodeReader_vendor/src/quirc/decode.c b/lib/ESP32QRCodeReader_vendor/src/quirc/decode.c new file mode 100644 index 0000000..67c8ed5 --- /dev/null +++ b/lib/ESP32QRCodeReader_vendor/src/quirc/decode.c @@ -0,0 +1,983 @@ +/* quirc -- QR-code recognition library + * Copyright (C) 2010-2012 Daniel Beer + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "quirc_internal.h" + +#include +#include + +#define MAX_POLY 64 + +/************************************************************************ + * Galois fields + */ + +struct galois_field +{ + int p; + const uint8_t *log; + const uint8_t *exp; +} __attribute__((aligned(8))); + +static const uint8_t gf16_exp[16] = { + 0x01, 0x02, 0x04, 0x08, 0x03, 0x06, 0x0c, 0x0b, + 0x05, 0x0a, 0x07, 0x0e, 0x0f, 0x0d, 0x09, 0x01}; + +static const uint8_t gf16_log[16] = { + 0x00, 0x0f, 0x01, 0x04, 0x02, 0x08, 0x05, 0x0a, + 0x03, 0x0e, 0x09, 0x07, 0x06, 0x0d, 0x0b, 0x0c}; + +static const struct galois_field gf16 = { + .p = 15, + .log = gf16_log, + .exp = gf16_exp}; + +static const uint8_t gf256_exp[256] = { + 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, + 0x1d, 0x3a, 0x74, 0xe8, 0xcd, 0x87, 0x13, 0x26, + 0x4c, 0x98, 0x2d, 0x5a, 0xb4, 0x75, 0xea, 0xc9, + 0x8f, 0x03, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0, + 0x9d, 0x27, 0x4e, 0x9c, 0x25, 0x4a, 0x94, 0x35, + 0x6a, 0xd4, 0xb5, 0x77, 0xee, 0xc1, 0x9f, 0x23, + 0x46, 0x8c, 0x05, 0x0a, 0x14, 0x28, 0x50, 0xa0, + 0x5d, 0xba, 0x69, 0xd2, 0xb9, 0x6f, 0xde, 0xa1, + 0x5f, 0xbe, 0x61, 0xc2, 0x99, 0x2f, 0x5e, 0xbc, + 0x65, 0xca, 0x89, 0x0f, 0x1e, 0x3c, 0x78, 0xf0, + 0xfd, 0xe7, 0xd3, 0xbb, 0x6b, 0xd6, 0xb1, 0x7f, + 0xfe, 0xe1, 0xdf, 0xa3, 0x5b, 0xb6, 0x71, 0xe2, + 0xd9, 0xaf, 0x43, 0x86, 0x11, 0x22, 0x44, 0x88, + 0x0d, 0x1a, 0x34, 0x68, 0xd0, 0xbd, 0x67, 0xce, + 0x81, 0x1f, 0x3e, 0x7c, 0xf8, 0xed, 0xc7, 0x93, + 0x3b, 0x76, 0xec, 0xc5, 0x97, 0x33, 0x66, 0xcc, + 0x85, 0x17, 0x2e, 0x5c, 0xb8, 0x6d, 0xda, 0xa9, + 0x4f, 0x9e, 0x21, 0x42, 0x84, 0x15, 0x2a, 0x54, + 0xa8, 0x4d, 0x9a, 0x29, 0x52, 0xa4, 0x55, 0xaa, + 0x49, 0x92, 0x39, 0x72, 0xe4, 0xd5, 0xb7, 0x73, + 0xe6, 0xd1, 0xbf, 0x63, 0xc6, 0x91, 0x3f, 0x7e, + 0xfc, 0xe5, 0xd7, 0xb3, 0x7b, 0xf6, 0xf1, 0xff, + 0xe3, 0xdb, 0xab, 0x4b, 0x96, 0x31, 0x62, 0xc4, + 0x95, 0x37, 0x6e, 0xdc, 0xa5, 0x57, 0xae, 0x41, + 0x82, 0x19, 0x32, 0x64, 0xc8, 0x8d, 0x07, 0x0e, + 0x1c, 0x38, 0x70, 0xe0, 0xdd, 0xa7, 0x53, 0xa6, + 0x51, 0xa2, 0x59, 0xb2, 0x79, 0xf2, 0xf9, 0xef, + 0xc3, 0x9b, 0x2b, 0x56, 0xac, 0x45, 0x8a, 0x09, + 0x12, 0x24, 0x48, 0x90, 0x3d, 0x7a, 0xf4, 0xf5, + 0xf7, 0xf3, 0xfb, 0xeb, 0xcb, 0x8b, 0x0b, 0x16, + 0x2c, 0x58, 0xb0, 0x7d, 0xfa, 0xe9, 0xcf, 0x83, + 0x1b, 0x36, 0x6c, 0xd8, 0xad, 0x47, 0x8e, 0x01}; + +static const uint8_t gf256_log[256] = { + 0x00, 0xff, 0x01, 0x19, 0x02, 0x32, 0x1a, 0xc6, + 0x03, 0xdf, 0x33, 0xee, 0x1b, 0x68, 0xc7, 0x4b, + 0x04, 0x64, 0xe0, 0x0e, 0x34, 0x8d, 0xef, 0x81, + 0x1c, 0xc1, 0x69, 0xf8, 0xc8, 0x08, 0x4c, 0x71, + 0x05, 0x8a, 0x65, 0x2f, 0xe1, 0x24, 0x0f, 0x21, + 0x35, 0x93, 0x8e, 0xda, 0xf0, 0x12, 0x82, 0x45, + 0x1d, 0xb5, 0xc2, 0x7d, 0x6a, 0x27, 0xf9, 0xb9, + 0xc9, 0x9a, 0x09, 0x78, 0x4d, 0xe4, 0x72, 0xa6, + 0x06, 0xbf, 0x8b, 0x62, 0x66, 0xdd, 0x30, 0xfd, + 0xe2, 0x98, 0x25, 0xb3, 0x10, 0x91, 0x22, 0x88, + 0x36, 0xd0, 0x94, 0xce, 0x8f, 0x96, 0xdb, 0xbd, + 0xf1, 0xd2, 0x13, 0x5c, 0x83, 0x38, 0x46, 0x40, + 0x1e, 0x42, 0xb6, 0xa3, 0xc3, 0x48, 0x7e, 0x6e, + 0x6b, 0x3a, 0x28, 0x54, 0xfa, 0x85, 0xba, 0x3d, + 0xca, 0x5e, 0x9b, 0x9f, 0x0a, 0x15, 0x79, 0x2b, + 0x4e, 0xd4, 0xe5, 0xac, 0x73, 0xf3, 0xa7, 0x57, + 0x07, 0x70, 0xc0, 0xf7, 0x8c, 0x80, 0x63, 0x0d, + 0x67, 0x4a, 0xde, 0xed, 0x31, 0xc5, 0xfe, 0x18, + 0xe3, 0xa5, 0x99, 0x77, 0x26, 0xb8, 0xb4, 0x7c, + 0x11, 0x44, 0x92, 0xd9, 0x23, 0x20, 0x89, 0x2e, + 0x37, 0x3f, 0xd1, 0x5b, 0x95, 0xbc, 0xcf, 0xcd, + 0x90, 0x87, 0x97, 0xb2, 0xdc, 0xfc, 0xbe, 0x61, + 0xf2, 0x56, 0xd3, 0xab, 0x14, 0x2a, 0x5d, 0x9e, + 0x84, 0x3c, 0x39, 0x53, 0x47, 0x6d, 0x41, 0xa2, + 0x1f, 0x2d, 0x43, 0xd8, 0xb7, 0x7b, 0xa4, 0x76, + 0xc4, 0x17, 0x49, 0xec, 0x7f, 0x0c, 0x6f, 0xf6, + 0x6c, 0xa1, 0x3b, 0x52, 0x29, 0x9d, 0x55, 0xaa, + 0xfb, 0x60, 0x86, 0xb1, 0xbb, 0xcc, 0x3e, 0x5a, + 0xcb, 0x59, 0x5f, 0xb0, 0x9c, 0xa9, 0xa0, 0x51, + 0x0b, 0xf5, 0x16, 0xeb, 0x7a, 0x75, 0x2c, 0xd7, + 0x4f, 0xae, 0xd5, 0xe9, 0xe6, 0xe7, 0xad, 0xe8, + 0x74, 0xd6, 0xf4, 0xea, 0xa8, 0x50, 0x58, 0xaf}; + +const static struct galois_field gf256 = { + .p = 255, + .log = gf256_log, + .exp = gf256_exp}; + +/************************************************************************ + * Polynomial operations + */ + +static void poly_add(uint8_t *dst, const uint8_t *src, uint8_t c, + int shift, const struct galois_field *gf) +{ + int i; + int log_c = gf->log[c]; + + if (!c) + return; + + for (i = 0; i < MAX_POLY; i++) + { + int p = i + shift; + uint8_t v = src[i]; + + if (p < 0 || p >= MAX_POLY) + continue; + if (!v) + continue; + + dst[p] ^= gf->exp[(gf->log[v] + log_c) % gf->p]; + } +} + +static uint8_t poly_eval(const uint8_t *s, uint8_t x, + const struct galois_field *gf) +{ + int i; + uint8_t sum = 0; + uint8_t log_x = gf->log[x]; + + if (!x) + return s[0]; + + for (i = 0; i < MAX_POLY; i++) + { + uint8_t c = s[i]; + + if (!c) + continue; + + sum ^= gf->exp[(gf->log[c] + log_x * i) % gf->p]; + } + + return sum; +} + +/************************************************************************ + * Berlekamp-Massey algorithm for finding error locator polynomials. + */ + +static void berlekamp_massey(const uint8_t *s, int N, + const struct galois_field *gf, + uint8_t *sigma) +{ + uint8_t C[MAX_POLY]; + uint8_t B[MAX_POLY]; + int L = 0; + int m = 1; + uint8_t b = 1; + int n; + + memset(B, 0, sizeof(B)); + memset(C, 0, sizeof(C)); + B[0] = 1; + C[0] = 1; + + for (n = 0; n < N; n++) + { + uint8_t d = s[n]; + uint8_t mult; + int i; + + for (i = 1; i <= L; i++) + { + if (!(C[i] && s[n - i])) + continue; + + d ^= gf->exp[(gf->log[C[i]] + + gf->log[s[n - i]]) % + gf->p]; + } + + mult = gf->exp[(gf->p - gf->log[b] + gf->log[d]) % gf->p]; + + if (!d) + { + m++; + } + else if (L * 2 <= n) + { + uint8_t T[MAX_POLY]; + + memcpy(T, C, sizeof(T)); + poly_add(C, B, mult, m, gf); + memcpy(B, T, sizeof(B)); + L = n + 1 - L; + b = d; + m = 1; + } + else + { + poly_add(C, B, mult, m, gf); + m++; + } + } + + memcpy(sigma, C, MAX_POLY); +} + +/************************************************************************ + * Code stream error correction + * + * Generator polynomial for GF(2^8) is x^8 + x^4 + x^3 + x^2 + 1 + */ + +static int block_syndromes(const uint8_t *data, int bs, int npar, uint8_t *s) +{ + int nonzero = 0; + int i; + + memset(s, 0, MAX_POLY); + + for (i = 0; i < npar; i++) + { + int j; + + for (j = 0; j < bs; j++) + { + uint8_t c = data[bs - j - 1]; + + if (!c) + continue; + + s[i] ^= gf256_exp[((int)gf256_log[c] + + i * j) % + 255]; + } + + if (s[i]) + nonzero = 1; + } + + return nonzero; +} + +static void eloc_poly(uint8_t *omega, + const uint8_t *s, const uint8_t *sigma, + int npar) +{ + int i; + + memset(omega, 0, MAX_POLY); + + for (i = 0; i < npar; i++) + { + const uint8_t a = sigma[i]; + const uint8_t log_a = gf256_log[a]; + int j; + + if (!a) + continue; + + for (j = 0; j + 1 < MAX_POLY; j++) + { + const uint8_t b = s[j + 1]; + + if (i + j >= npar) + break; + + if (!b) + continue; + + omega[i + j] ^= + gf256_exp[(log_a + gf256_log[b]) % 255]; + } + } +} + +static quirc_decode_error_t correct_block(uint8_t *data, + const struct quirc_rs_params *ecc) +{ + int npar = ecc->bs - ecc->dw; + uint8_t s[MAX_POLY]; + uint8_t sigma[MAX_POLY]; + uint8_t sigma_deriv[MAX_POLY]; + uint8_t omega[MAX_POLY]; + int i; + + /* Compute syndrome vector */ + if (!block_syndromes(data, ecc->bs, npar, s)) + return QUIRC_SUCCESS; + + berlekamp_massey(s, npar, &gf256, sigma); + + /* Compute derivative of sigma */ + memset(sigma_deriv, 0, MAX_POLY); + for (i = 0; i + 1 < MAX_POLY; i += 2) + sigma_deriv[i] = sigma[i + 1]; + + /* Compute error evaluator polynomial */ + eloc_poly(omega, s, sigma, npar - 1); + + /* Find error locations and magnitudes */ + for (i = 0; i < ecc->bs; i++) + { + uint8_t xinv = gf256_exp[255 - i]; + + if (!poly_eval(sigma, xinv, &gf256)) + { + uint8_t sd_x = poly_eval(sigma_deriv, xinv, &gf256); + uint8_t omega_x = poly_eval(omega, xinv, &gf256); + uint8_t error = gf256_exp[(255 - gf256_log[sd_x] + + gf256_log[omega_x]) % + 255]; + + data[ecc->bs - i - 1] ^= error; + } + } + + if (block_syndromes(data, ecc->bs, npar, s)) + return QUIRC_ERROR_DATA_ECC; + + return QUIRC_SUCCESS; +} + +/************************************************************************ + * Format value error correction + * + * Generator polynomial for GF(2^4) is x^4 + x + 1 + */ + +#define FORMAT_MAX_ERROR 3 +#define FORMAT_SYNDROMES (FORMAT_MAX_ERROR * 2) +#define FORMAT_BITS 15 + +static int format_syndromes(uint16_t u, uint8_t *s) +{ + int i; + int nonzero = 0; + + memset(s, 0, MAX_POLY); + + for (i = 0; i < FORMAT_SYNDROMES; i++) + { + int j; + + s[i] = 0; + for (j = 0; j < FORMAT_BITS; j++) + if (u & (1 << j)) + s[i] ^= gf16_exp[((i + 1) * j) % 15]; + + if (s[i]) + nonzero = 1; + } + + return nonzero; +} + +static quirc_decode_error_t correct_format(uint16_t *f_ret) +{ + uint16_t u = *f_ret; + int i; + uint8_t s[MAX_POLY]; + uint8_t sigma[MAX_POLY]; + + /* Evaluate U (received codeword) at each of alpha_1 .. alpha_6 + * to get S_1 .. S_6 (but we index them from 0). + */ + if (!format_syndromes(u, s)) + return QUIRC_SUCCESS; + + berlekamp_massey(s, FORMAT_SYNDROMES, &gf16, sigma); + + /* Now, find the roots of the polynomial */ + for (i = 0; i < 15; i++) + if (!poly_eval(sigma, gf16_exp[15 - i], &gf16)) + u ^= (1 << i); + + if (format_syndromes(u, s)) + return QUIRC_ERROR_FORMAT_ECC; + + *f_ret = u; + return QUIRC_SUCCESS; +} + +/************************************************************************ + * Decoder algorithm + */ + +struct datastream +{ + uint8_t raw[QUIRC_MAX_PAYLOAD]; + int data_bits; + int ptr; + + uint8_t data[QUIRC_MAX_PAYLOAD]; +} __attribute__((aligned(8))); + +static inline int grid_bit(const struct quirc_code *code, int x, int y) +{ + int p = y * code->size + x; + + return (code->cell_bitmap[p >> 3] >> (p & 7)) & 1; +} + +static quirc_decode_error_t read_format(const struct quirc_code *code, + struct quirc_data *data, int which) +{ + int i; + uint16_t format = 0; + uint16_t fdata; + quirc_decode_error_t err; + + if (which) + { + for (i = 0; i < 7; i++) + format = (format << 1) | + grid_bit(code, 8, code->size - 1 - i); + for (i = 0; i < 8; i++) + format = (format << 1) | + grid_bit(code, code->size - 8 + i, 8); + } + else + { + static const int xs[15] = { + 8, 8, 8, 8, 8, 8, 8, 8, 7, 5, 4, 3, 2, 1, 0}; + static const int ys[15] = { + 0, 1, 2, 3, 4, 5, 7, 8, 8, 8, 8, 8, 8, 8, 8}; + + for (i = 14; i >= 0; i--) + format = (format << 1) | grid_bit(code, xs[i], ys[i]); + } + + format ^= 0x5412; + + err = correct_format(&format); + if (err) + return err; + + fdata = format >> 10; + data->ecc_level = fdata >> 3; + data->mask = fdata & 7; + + return QUIRC_SUCCESS; +} + +static int mask_bit(int mask, int i, int j) +{ + switch (mask) + { + case 0: + return !((i + j) % 2); + case 1: + return !(i % 2); + case 2: + return !(j % 3); + case 3: + return !((i + j) % 3); + case 4: + return !(((i / 2) + (j / 3)) % 2); + case 5: + return !((i * j) % 2 + (i * j) % 3); + case 6: + return !(((i * j) % 2 + (i * j) % 3) % 2); + case 7: + return !(((i * j) % 3 + (i + j) % 2) % 2); + } + + return 0; +} + +static int reserved_cell(int version, int i, int j) +{ + const struct quirc_version_info *ver = &quirc_version_db[version]; + int size = version * 4 + 17; + int ai = -1, aj = -1, a; + + /* Finder + format: top left */ + if (i < 9 && j < 9) + return 1; + + /* Finder + format: bottom left */ + if (i + 8 >= size && j < 9) + return 1; + + /* Finder + format: top right */ + if (i < 9 && j + 8 >= size) + return 1; + + /* Exclude timing patterns */ + if (i == 6 || j == 6) + return 1; + + /* Exclude version info, if it exists. Version info sits adjacent to + * the top-right and bottom-left finders in three rows, bounded by + * the timing pattern. + */ + if (version >= 7) + { + if (i < 6 && j + 11 >= size) + return 1; + if (i + 11 >= size && j < 6) + return 1; + } + + /* Exclude alignment patterns */ + for (a = 0; a < QUIRC_MAX_ALIGNMENT && ver->apat[a]; a++) + { + int p = ver->apat[a]; + + if (abs(p - i) < 3) + ai = a; + if (abs(p - j) < 3) + aj = a; + } + + if (ai >= 0 && aj >= 0) + { + a--; + if (ai > 0 && ai < a) + return 1; + if (aj > 0 && aj < a) + return 1; + if (aj == a && ai == a) + return 1; + } + + return 0; +} + +static void read_bit(const struct quirc_code *code, + struct quirc_data *data, + struct datastream *ds, int i, int j) +{ + int bitpos = ds->data_bits & 7; + int bytepos = ds->data_bits >> 3; + int v = grid_bit(code, j, i); + + if (mask_bit(data->mask, i, j)) + v ^= 1; + + if (v) + ds->raw[bytepos] |= (0x80 >> bitpos); + + ds->data_bits++; +} + +static void read_data(const struct quirc_code *code, + struct quirc_data *data, + struct datastream *ds) +{ + int y = code->size - 1; + int x = code->size - 1; + int dir = -1; + + while (x > 0) + { + if (x == 6) + x--; + + if (!reserved_cell(data->version, y, x)) + read_bit(code, data, ds, y, x); + + if (!reserved_cell(data->version, y, x - 1)) + read_bit(code, data, ds, y, x - 1); + + y += dir; + if (y < 0 || y >= code->size) + { + dir = -dir; + x -= 2; + y += dir; + } + } +} + +static quirc_decode_error_t codestream_ecc(struct quirc_data *data, + struct datastream *ds) +{ + const struct quirc_version_info *ver = + &quirc_version_db[data->version]; + const struct quirc_rs_params *sb_ecc = &ver->ecc[data->ecc_level]; + struct quirc_rs_params lb_ecc; + const int lb_count = + (ver->data_bytes - sb_ecc->bs * sb_ecc->ns) / (sb_ecc->bs + 1); + const int bc = lb_count + sb_ecc->ns; + const int ecc_offset = sb_ecc->dw * bc + lb_count; + int dst_offset = 0; + int i; + + memcpy(&lb_ecc, sb_ecc, sizeof(lb_ecc)); + lb_ecc.dw++; + lb_ecc.bs++; + + for (i = 0; i < bc; i++) + { + uint8_t *dst = ds->data + dst_offset; + const struct quirc_rs_params *ecc = + (i < sb_ecc->ns) ? sb_ecc : &lb_ecc; + const int num_ec = ecc->bs - ecc->dw; + quirc_decode_error_t err; + int j; + + for (j = 0; j < ecc->dw; j++) + dst[j] = ds->raw[j * bc + i]; + for (j = 0; j < num_ec; j++) + dst[ecc->dw + j] = ds->raw[ecc_offset + j * bc + i]; + + err = correct_block(dst, ecc); + if (err) + return err; + + dst_offset += ecc->dw; + } + + ds->data_bits = dst_offset * 8; + + return QUIRC_SUCCESS; +} + +static inline int bits_remaining(const struct datastream *ds) +{ + return ds->data_bits - ds->ptr; +} + +static int take_bits(struct datastream *ds, int len) +{ + int ret = 0; + + while (len && (ds->ptr < ds->data_bits)) + { + uint8_t b = ds->data[ds->ptr >> 3]; + int bitpos = ds->ptr & 7; + + ret <<= 1; + if ((b << bitpos) & 0x80) + ret |= 1; + + ds->ptr++; + len--; + } + + return ret; +} + +static int numeric_tuple(struct quirc_data *data, + struct datastream *ds, + int bits, int digits) +{ + int tuple; + int i; + + if (bits_remaining(ds) < bits) + return -1; + + tuple = take_bits(ds, bits); + + for (i = digits - 1; i >= 0; i--) + { + data->payload[data->payload_len + i] = tuple % 10 + '0'; + tuple /= 10; + } + + data->payload_len += digits; + return 0; +} + +static quirc_decode_error_t decode_numeric(struct quirc_data *data, + struct datastream *ds) +{ + int bits = 14; + int count; + + if (data->version < 10) + bits = 10; + else if (data->version < 27) + bits = 12; + + count = take_bits(ds, bits); + if (data->payload_len + count + 1 > QUIRC_MAX_PAYLOAD) + return QUIRC_ERROR_DATA_OVERFLOW; + + while (count >= 3) + { + if (numeric_tuple(data, ds, 10, 3) < 0) + return QUIRC_ERROR_DATA_UNDERFLOW; + count -= 3; + } + + if (count >= 2) + { + if (numeric_tuple(data, ds, 7, 2) < 0) + return QUIRC_ERROR_DATA_UNDERFLOW; + count -= 2; + } + + if (count) + { + if (numeric_tuple(data, ds, 4, 1) < 0) + return QUIRC_ERROR_DATA_UNDERFLOW; + count--; + } + + return QUIRC_SUCCESS; +} + +static int alpha_tuple(struct quirc_data *data, + struct datastream *ds, + int bits, int digits) +{ + int tuple; + int i; + + if (bits_remaining(ds) < bits) + return -1; + + tuple = take_bits(ds, bits); + + for (i = 0; i < digits; i++) + { + static const char *alpha_map = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"; + + data->payload[data->payload_len + digits - i - 1] = + alpha_map[tuple % 45]; + tuple /= 45; + } + + data->payload_len += digits; + return 0; +} + +static quirc_decode_error_t decode_alpha(struct quirc_data *data, + struct datastream *ds) +{ + int bits = 13; + int count; + + if (data->version < 10) + bits = 9; + else if (data->version < 27) + bits = 11; + + count = take_bits(ds, bits); + if (data->payload_len + count + 1 > QUIRC_MAX_PAYLOAD) + return QUIRC_ERROR_DATA_OVERFLOW; + + while (count >= 2) + { + if (alpha_tuple(data, ds, 11, 2) < 0) + return QUIRC_ERROR_DATA_UNDERFLOW; + count -= 2; + } + + if (count) + { + if (alpha_tuple(data, ds, 6, 1) < 0) + return QUIRC_ERROR_DATA_UNDERFLOW; + count--; + } + + return QUIRC_SUCCESS; +} + +static quirc_decode_error_t decode_byte(struct quirc_data *data, + struct datastream *ds) +{ + int bits = 16; + int count; + int i; + + if (data->version < 10) + bits = 8; + + count = take_bits(ds, bits); + if (data->payload_len + count + 1 > QUIRC_MAX_PAYLOAD) + return QUIRC_ERROR_DATA_OVERFLOW; + if (bits_remaining(ds) < count * 8) + return QUIRC_ERROR_DATA_UNDERFLOW; + + for (i = 0; i < count; i++) + data->payload[data->payload_len++] = take_bits(ds, 8); + + return QUIRC_SUCCESS; +} + +static quirc_decode_error_t decode_kanji(struct quirc_data *data, + struct datastream *ds) +{ + int bits = 12; + int count; + int i; + + if (data->version < 10) + bits = 8; + else if (data->version < 27) + bits = 10; + + count = take_bits(ds, bits); + if (data->payload_len + count * 2 + 1 > QUIRC_MAX_PAYLOAD) + return QUIRC_ERROR_DATA_OVERFLOW; + if (bits_remaining(ds) < count * 13) + return QUIRC_ERROR_DATA_UNDERFLOW; + + for (i = 0; i < count; i++) + { + int d = take_bits(ds, 13); + int msB = d / 0xc0; + int lsB = d % 0xc0; + int intermediate = (msB << 8) | lsB; + uint16_t sjw; + + if (intermediate + 0x8140 <= 0x9ffc) + { + /* bytes are in the range 0x8140 to 0x9FFC */ + sjw = intermediate + 0x8140; + } + else + { + /* bytes are in the range 0xE040 to 0xEBBF */ + sjw = intermediate + 0xc140; + } + + data->payload[data->payload_len++] = sjw >> 8; + data->payload[data->payload_len++] = sjw & 0xff; + } + + return QUIRC_SUCCESS; +} + +static quirc_decode_error_t decode_eci(struct quirc_data *data, + struct datastream *ds) +{ + if (bits_remaining(ds) < 8) + return QUIRC_ERROR_DATA_UNDERFLOW; + + data->eci = take_bits(ds, 8); + + if ((data->eci & 0xc0) == 0x80) + { + if (bits_remaining(ds) < 8) + return QUIRC_ERROR_DATA_UNDERFLOW; + + data->eci = (data->eci << 8) | take_bits(ds, 8); + } + else if ((data->eci & 0xe0) == 0xc0) + { + if (bits_remaining(ds) < 16) + return QUIRC_ERROR_DATA_UNDERFLOW; + + data->eci = (data->eci << 16) | take_bits(ds, 16); + } + + return QUIRC_SUCCESS; +} + +static quirc_decode_error_t decode_payload(struct quirc_data *data, + struct datastream *ds) +{ + while (bits_remaining(ds) >= 4) + { + quirc_decode_error_t err = QUIRC_SUCCESS; + int type = take_bits(ds, 4); + + switch (type) + { + case QUIRC_DATA_TYPE_NUMERIC: + err = decode_numeric(data, ds); + break; + + case QUIRC_DATA_TYPE_ALPHA: + err = decode_alpha(data, ds); + break; + + case QUIRC_DATA_TYPE_BYTE: + err = decode_byte(data, ds); + break; + + case QUIRC_DATA_TYPE_KANJI: + err = decode_kanji(data, ds); + break; + + case 7: + err = decode_eci(data, ds); + break; + + default: + goto done; + } + + if (err) + return err; + + if (!(type & (type - 1)) && (type > data->data_type)) + data->data_type = type; + } + +done: + + /* Add nul terminator to all payloads */ + if (data->payload_len >= sizeof(data->payload)) + data->payload_len--; + data->payload[data->payload_len] = 0; + + return QUIRC_SUCCESS; +} + +quirc_decode_error_t quirc_decode(const struct quirc_code *code, + struct quirc_data *data) +{ + quirc_decode_error_t err; + struct datastream *ds = ps_malloc(sizeof(struct datastream)); + + if ((code->size - 17) % 4) + { + free(ds); + return QUIRC_ERROR_INVALID_GRID_SIZE; + } + + memset(data, 0, sizeof(*data)); + memset(ds, 0, sizeof(*ds)); + + data->version = (code->size - 17) / 4; + + if (data->version < 1 || + data->version > QUIRC_MAX_VERSION) + { + free(ds); + return QUIRC_ERROR_INVALID_VERSION; + } + + /* Read format information -- try both locations */ + err = read_format(code, data, 0); + if (err) + err = read_format(code, data, 1); + if (err) + { + free(ds); + return err; + } + + read_data(code, data, ds); + err = codestream_ecc(data, ds); + if (err) + { + free(ds); + return err; + } + + err = decode_payload(data, ds); + if (err) + { + free(ds); + return err; + } + + free(ds); + return QUIRC_SUCCESS; +} \ No newline at end of file diff --git a/lib/ESP32QRCodeReader_vendor/src/quirc/identify.c b/lib/ESP32QRCodeReader_vendor/src/quirc/identify.c new file mode 100644 index 0000000..5ddb788 --- /dev/null +++ b/lib/ESP32QRCodeReader_vendor/src/quirc/identify.c @@ -0,0 +1,1286 @@ +/* quirc - QR-code recognition library + * Copyright (C) 2010-2012 Daniel Beer + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include + +#include "openmv/fmath.h" +#include "openmv/collections.h" +#include "quirc_internal.h" + +/************************************************************************ + * Linear algebra routines + */ + +static int line_intersect(const struct quirc_point *p0, + const struct quirc_point *p1, + const struct quirc_point *q0, + const struct quirc_point *q1, + struct quirc_point *r) +{ + /* (a, b) is perpendicular to line p */ + int a = -(p1->y - p0->y); + int b = p1->x - p0->x; + + /* (c, d) is perpendicular to line q */ + int c = -(q1->y - q0->y); + int d = q1->x - q0->x; + + /* e and f are dot products of the respective vectors with p and q */ + int e = a * p1->x + b * p1->y; + int f = c * q1->x + d * q1->y; + + /* Now we need to solve: + * [a b] [rx] [e] + * [c d] [ry] = [f] + * + * We do this by inverting the matrix and applying it to (e, f): + * [ d -b] [e] [rx] + * 1/det [-c a] [f] = [ry] + */ + int det = (a * d) - (b * c); + + if (!det) + return 0; + + r->x = (d * e - b * f) / det; + r->y = (-c * e + a * f) / det; + + return 1; +} + +static void perspective_setup(float *c, + const struct quirc_point *rect, + float w, float h) +{ + float x0 = rect[0].x; + float y0 = rect[0].y; + float x1 = rect[1].x; + float y1 = rect[1].y; + float x2 = rect[2].x; + float y2 = rect[2].y; + float x3 = rect[3].x; + float y3 = rect[3].y; + + float wden = w * (x2 * y3 - x3 * y2 + (x3 - x2) * y1 + x1 * (y2 - y3)); + float hden = h * (x2 * y3 + x1 * (y2 - y3) - x3 * y2 + (x3 - x2) * y1); + + c[0] = (x1 * (x2 * y3 - x3 * y2) + x0 * (-x2 * y3 + x3 * y2 + (x2 - x3) * y1) + + x1 * (x3 - x2) * y0) / + wden; + c[1] = -(x0 * (x2 * y3 + x1 * (y2 - y3) - x2 * y1) - x1 * x3 * y2 + x2 * x3 * y1 + (x1 * x3 - x2 * x3) * y0) / hden; + c[2] = x0; + c[3] = (y0 * (x1 * (y3 - y2) - x2 * y3 + x3 * y2) + y1 * (x2 * y3 - x3 * y2) + + x0 * y1 * (y2 - y3)) / + wden; + c[4] = (x0 * (y1 * y3 - y2 * y3) + x1 * y2 * y3 - x2 * y1 * y3 + + y0 * (x3 * y2 - x1 * y2 + (x2 - x3) * y1)) / + hden; + c[5] = y0; + c[6] = (x1 * (y3 - y2) + x0 * (y2 - y3) + (x2 - x3) * y1 + (x3 - x2) * y0) / wden; + c[7] = (-x2 * y3 + x1 * y3 + x3 * y2 + x0 * (y1 - y2) - x3 * y1 + (x2 - x1) * y0) / + hden; +} + +static void perspective_map(const float *c, + float u, float v, struct quirc_point *ret) +{ + float den = c[6] * u + c[7] * v + 1.0; + float x = (c[0] * u + c[1] * v + c[2]) / den; + float y = (c[3] * u + c[4] * v + c[5]) / den; + + ret->x = fast_roundf(x); + ret->y = fast_roundf(y); +} + +static void perspective_unmap(const float *c, + const struct quirc_point *in, + float *u, float *v) +{ + float x = in->x; + float y = in->y; + float den = -c[0] * c[7] * y + c[1] * c[6] * y + (c[3] * c[7] - c[4] * c[6]) * x + + c[0] * c[4] - c[1] * c[3]; + + *u = -(c[1] * (y - c[5]) - c[2] * c[7] * y + (c[5] * c[7] - c[4]) * x + c[2] * c[4]) / + den; + *v = (c[0] * (y - c[5]) - c[2] * c[6] * y + (c[5] * c[6] - c[3]) * x + c[2] * c[3]) / + den; +} + +/************************************************************************ + * Span-based floodfill routine + */ + +typedef void (*span_func_t)(void *user_data, int y, int left, int right); + +typedef struct xylf +{ + int16_t x, y, l, r; +} __attribute__((aligned(8))) +xylf_t; + +//计算该区域的面积,from是像素颜色,to是区块标号,user_data是申请的区块结构体,func是计算面积的函数 +static void flood_fill_seed(struct quirc *q, int x, int y, int from, int to, + span_func_t func, void *user_data, + int depth) +{ + (void)depth; // unused + + lifo_t lifo; + size_t lifo_len; + lifo_alloc_all(&lifo, &lifo_len, sizeof(xylf_t)); + //late in first out. 申请xylf_t的lifo,一次申请完,长度存储在lifo_len中 + + for (;;) + { + int left = x; + int right = x; + int i; + quirc_pixel_t *row = q->pixels + y * q->w; //行起始地址 + //查找左右边界 + while (left > 0 && row[left - 1] == from) + left--; + + while (right < q->w - 1 && row[right + 1] == from) + right++; + + /* Fill the extent 对应像素标记为区块号*/ + for (i = left; i <= right; i++) + row[i] = to; + //累加区域内的像素点数作为面积 + if (func) + func(user_data, y, left, right); + + for (;;) + { + if (lifo_size(&lifo) < lifo_len) + { //栈中的数量 + /* Seed new flood-fills */ + if (y > 0) + { //查找上一行有没有在同一区域的点 + row = q->pixels + (y - 1) * q->w; + + bool recurse = false; + for (i = left; i <= right; i++) + if (row[i] == from) + { //相同区域,则入栈原来的区块 + xylf_t context; + context.x = x; + context.y = y; + context.l = left; + context.r = right; + lifo_enqueue(&lifo, &context); + //mp_printf(&mp_plat_print, "#x=%x,y=%d;x1=%d,y1=%d\n",x,y,i,y-1); + x = i; + y = y - 1; + recurse = true; + break; + } + if (recurse) + break; + } + //查找下一行有没有在同一区域的点 + if (y < q->h - 1) + { + row = q->pixels + (y + 1) * q->w; + + bool recurse = false; + for (i = left; i <= right; i++) + if (row[i] == from) + { + xylf_t context; + context.x = x; + context.y = y; + context.l = left; + context.r = right; + lifo_enqueue(&lifo, &context); + //mp_printf(&mp_plat_print, "#x=%x,y=%d;x1=%d,y1=%d\n",x,y,i,y+1); + x = i; + y = y + 1; + recurse = true; + break; + } + if (recurse) + break; + } + } + + if (!lifo_size(&lifo)) + { + lifo_free(&lifo); //如果最起始为止就没找到,那么返回 + return; + } + //本次迭代,往上,往下找边界(相同颜色像素点),直到找不到为止 + //找到边界后,出栈上层像素点,回退回去 + xylf_t context; + lifo_dequeue(&lifo, &context); //这里存疑,如果都没有的话,dequeue就会反向溢出吧。。 + x = context.x; + y = context.y; + left = context.l; + right = context.r; + //mp_printf(&mp_plat_print, "#deq: x=%x,y=%d\n",x,y); + } //找到相同from,break到这外面 + } +} + +/************************************************************************ + * Adaptive thresholding + */ + +#define THRESHOLD_S_MIN 1 +#define THRESHOLD_S_DEN 8 +#define THRESHOLD_T 5 + +static void threshold(struct quirc *q) +{ + int x, y; + int avg_w = 0; + int avg_u = 0; + int threshold_s = q->w / THRESHOLD_S_DEN; + quirc_pixel_t *row = q->pixels; + + /* + * Ensure a sane, non-zero value for threshold_s. + * + * threshold_s can be zero if the image width is small. We need to avoid + * SIGFPE as it will be used as divisor. + */ + if (threshold_s < THRESHOLD_S_MIN) + threshold_s = THRESHOLD_S_MIN; + + for (y = 0; y < q->h; y++) + { + int row_average[q->w]; + + memset(row_average, 0, sizeof(row_average)); + + for (x = 0; x < q->w; x++) + { + int w, u; + + if (y & 1) + { + w = x; + u = q->w - 1 - x; + } + else + { + w = q->w - 1 - x; + u = x; + } + + avg_w = (avg_w * (threshold_s - 1)) / + threshold_s + + row[w]; + avg_u = (avg_u * (threshold_s - 1)) / + threshold_s + + row[u]; + + row_average[w] += avg_w; + row_average[u] += avg_u; + } + + for (x = 0; x < q->w; x++) + { + if (row[x] < row_average[x] * + (100 - THRESHOLD_T) / (200 * threshold_s)) + row[x] = QUIRC_PIXEL_BLACK; + else + row[x] = QUIRC_PIXEL_WHITE; + } + + row += q->w; + } +} + +static void area_count(void *user_data, int y, int left, int right) +{ + ((struct quirc_region *)user_data)->count += right - left + 1; +} + +static int region_code(struct quirc *q, int x, int y) +{ //region指的是QRcode的区域,成员为区域的坐标,像素面积,是否顶点 + int pixel; + struct quirc_region *box; + int region; + + if (x < 0 || y < 0 || x >= q->w || y >= q->h) + return -1; + + pixel = q->pixels[y * q->w + x]; + //预先判断非正常的像素情况,退出 + if (pixel >= QUIRC_PIXEL_REGION) + return pixel; + + if (pixel == QUIRC_PIXEL_WHITE) + return -1; + + if (q->num_regions >= QUIRC_MAX_REGIONS) + return -1; + //新建一个区域 + region = q->num_regions; + box = &q->regions[q->num_regions++]; + + memset(box, 0, sizeof(*box)); + + box->seed.x = x; + box->seed.y = y; + box->capstone = -1; + //计算该区域的面积 + flood_fill_seed(q, x, y, pixel, region, area_count, box, 0); + + return region; +} + +struct polygon_score_data +{ + struct quirc_point ref; + + int scores[4]; + struct quirc_point *corners; +} __attribute__((aligned(8))); + +static void find_one_corner(void *user_data, int y, int left, int right) +{ + struct polygon_score_data *psd = + (struct polygon_score_data *)user_data; + int xs[2] = {left, right}; + int dy = y - psd->ref.y; + int i; + + for (i = 0; i < 2; i++) + { + int dx = xs[i] - psd->ref.x; + int d = dx * dx + dy * dy; + + if (d > psd->scores[0]) + { + psd->scores[0] = d; + psd->corners[0].x = xs[i]; + psd->corners[0].y = y; + } + } +} + +static void find_other_corners(void *user_data, int y, int left, int right) +{ + struct polygon_score_data *psd = + (struct polygon_score_data *)user_data; + int xs[2] = {left, right}; + int i; + + for (i = 0; i < 2; i++) + { + int up = xs[i] * psd->ref.x + y * psd->ref.y; + int right = xs[i] * -psd->ref.y + y * psd->ref.x; + int scores[4] = {up, right, -up, -right}; + int j; + + for (j = 0; j < 4; j++) + { + if (scores[j] > psd->scores[j]) + { + psd->scores[j] = scores[j]; + psd->corners[j].x = xs[i]; + psd->corners[j].y = y; + } + } + } +} + +static void find_region_corners(struct quirc *q, + int rcode, const struct quirc_point *ref, + struct quirc_point *corners) +{ + struct quirc_region *region = &q->regions[rcode]; + struct polygon_score_data psd; + int i; + + memset(&psd, 0, sizeof(psd)); + psd.corners = corners; + + memcpy(&psd.ref, ref, sizeof(psd.ref)); + psd.scores[0] = -1; + flood_fill_seed(q, region->seed.x, region->seed.y, + rcode, QUIRC_PIXEL_BLACK, + find_one_corner, &psd, 0); + + psd.ref.x = psd.corners[0].x - psd.ref.x; + psd.ref.y = psd.corners[0].y - psd.ref.y; + + for (i = 0; i < 4; i++) + memcpy(&psd.corners[i], ®ion->seed, + sizeof(psd.corners[i])); + + i = region->seed.x * psd.ref.x + region->seed.y * psd.ref.y; + psd.scores[0] = i; + psd.scores[2] = -i; + i = region->seed.x * -psd.ref.y + region->seed.y * psd.ref.x; + psd.scores[1] = i; + psd.scores[3] = -i; + + flood_fill_seed(q, region->seed.x, region->seed.y, + QUIRC_PIXEL_BLACK, rcode, + find_other_corners, &psd, 0); +} + +static void record_capstone(struct quirc *q, int ring, int stone) +{ + struct quirc_region *stone_reg = &q->regions[stone]; + struct quirc_region *ring_reg = &q->regions[ring]; + struct quirc_capstone *capstone; + int cs_index; + + if (q->num_capstones >= QUIRC_MAX_CAPSTONES) + return; + + cs_index = q->num_capstones; + capstone = &q->capstones[q->num_capstones++]; + + memset(capstone, 0, sizeof(*capstone)); + + capstone->qr_grid = -1; + capstone->ring = ring; + capstone->stone = stone; + stone_reg->capstone = cs_index; + ring_reg->capstone = cs_index; + + /* Find the corners of the ring */ + find_region_corners(q, ring, &stone_reg->seed, capstone->corners); + + /* Set up the perspective transform and find the center */ + perspective_setup(capstone->c, capstone->corners, 7.0, 7.0); + perspective_map(capstone->c, 3.5, 3.5, &capstone->center); +} + +static void test_capstone(struct quirc *q, int x, int y, int *pb) +{ + int ring_right = region_code(q, x - pb[4], y); //x-pb[4]是标记环右边的左侧 + int stone = region_code(q, x - pb[4] - pb[3] - pb[2], y); //实心点左侧 + int ring_left = region_code(q, x - pb[4] - pb[3] - pb[2] - pb[1] - pb[0], y); //环左侧 + struct quirc_region *stone_reg; + struct quirc_region *ring_reg; + int ratio; + //以下检测顶点标记是否符合规范,环称为ring,中间称为stone + if (ring_left < 0 || ring_right < 0 || stone < 0) + return; + + /* Left and ring of ring should be connected */ + if (ring_left != ring_right) + return; + + /* Ring should be disconnected from stone */ + if (ring_left == stone) + return; + + stone_reg = &q->regions[stone]; + ring_reg = &q->regions[ring_left]; + + /* Already detected */ + if (stone_reg->capstone >= 0 || ring_reg->capstone >= 0) + return; + + /* Ratio should ideally be 37.5 中间实心点占面积比例应该在37.5%左右*/ + ratio = stone_reg->count * 100 / ring_reg->count; + if (ratio < 10 || ratio > 70) + return; + + record_capstone(q, ring_left, stone); +} + +static void finder_scan(struct quirc *q, int y) +{ + quirc_pixel_t *row = q->pixels + y * q->w; + int x; + int last_color = 0; + int run_length = 0; + int run_count = 0; + int pb[5]; //means QRcode's pixel width + + memset(pb, 0, sizeof(pb)); + for (x = 0; x < q->w; x++) + { + int color = row[x] ? 1 : 0; + + if (x && color != last_color) + { // color is different + memmove(pb, pb + 1, sizeof(pb[0]) * 4); //left move in one data + pb[4] = run_length; //run how many pix to get different color + run_length = 0; + run_count++; //get more than 5 time color jump + + if (!color && run_count >= 5) + { // find the marker of QRcode(three corner's marker) + static int check[5] = {1, 1, 3, 1, 1}; + int avg, err; + int i; + int ok = 1; + + avg = (pb[0] + pb[1] + pb[3] + pb[4]) / 4; + err = avg * 3 / 4; + + for (i = 0; i < 5; i++) + if (pb[i] < check[i] * avg - err || + pb[i] > check[i] * avg + err) + ok = 0; + + if (ok) + test_capstone(q, x, y, pb); + } + } + + run_length++; + last_color = color; + } +} + +static void find_alignment_pattern(struct quirc *q, int index) +{ + struct quirc_grid *qr = &q->grids[index]; + struct quirc_capstone *c0 = &q->capstones[qr->caps[0]]; + struct quirc_capstone *c2 = &q->capstones[qr->caps[2]]; + struct quirc_point a; + struct quirc_point b; + struct quirc_point c; + int size_estimate; + int step_size = 1; + int dir = 0; + float u, v; + + /* Grab our previous estimate of the alignment pattern corner */ + memcpy(&b, &qr->align, sizeof(b)); + + /* Guess another two corners of the alignment pattern so that we + * can estimate its size. + */ + perspective_unmap(c0->c, &b, &u, &v); + perspective_map(c0->c, u, v + 1.0, &a); + perspective_unmap(c2->c, &b, &u, &v); + perspective_map(c2->c, u + 1.0, v, &c); + + size_estimate = abs((a.x - b.x) * -(c.y - b.y) + + (a.y - b.y) * (c.x - b.x)); + + /* Spiral outwards from the estimate point until we find something + * roughly the right size. Don't look too far from the estimate + * point. + */ + while (step_size * step_size < size_estimate * 100) + { + static const int dx_map[] = {1, 0, -1, 0}; + static const int dy_map[] = {0, -1, 0, 1}; + int i; + + for (i = 0; i < step_size; i++) + { + int code = region_code(q, b.x, b.y); + + if (code >= 0) + { + struct quirc_region *reg = &q->regions[code]; + + if (reg->count >= size_estimate / 2 && + reg->count <= size_estimate * 2) + { + qr->align_region = code; + return; + } + } + + b.x += dx_map[dir]; + b.y += dy_map[dir]; + } + + dir = (dir + 1) % 4; + if (!(dir & 1)) + step_size++; + } +} + +static void find_leftmost_to_line(void *user_data, int y, int left, int right) +{ + struct polygon_score_data *psd = + (struct polygon_score_data *)user_data; + int xs[2] = {left, right}; + int i; + + for (i = 0; i < 2; i++) + { + int d = -psd->ref.y * xs[i] + psd->ref.x * y; + + if (d < psd->scores[0]) + { + psd->scores[0] = d; + psd->corners[0].x = xs[i]; + psd->corners[0].y = y; + } + } +} + +/* Do a Bresenham scan from one point to another and count the number + * of black/white transitions. + */ +static int timing_scan(const struct quirc *q, + const struct quirc_point *p0, + const struct quirc_point *p1) +{ + int n = p1->x - p0->x; + int d = p1->y - p0->y; + int x = p0->x; + int y = p0->y; + int *dom, *nondom; + int dom_step; + int nondom_step; + int a = 0; + int i; + int run_length = 0; + int count = 0; + + if (p0->x < 0 || p0->y < 0 || p0->x >= q->w || p0->y >= q->h) + return -1; + if (p1->x < 0 || p1->y < 0 || p1->x >= q->w || p1->y >= q->h) + return -1; + + if (abs(n) > abs(d)) + { + int swap = n; + + n = d; + d = swap; + + dom = &x; + nondom = &y; + } + else + { + dom = &y; + nondom = &x; + } + + if (n < 0) + { + n = -n; + nondom_step = -1; + } + else + { + nondom_step = 1; + } + + if (d < 0) + { + d = -d; + dom_step = -1; + } + else + { + dom_step = 1; + } + + x = p0->x; + y = p0->y; + for (i = 0; i <= d; i++) + { + int pixel; + + if (y < 0 || y >= q->h || x < 0 || x >= q->w) + break; + + pixel = q->pixels[y * q->w + x]; + + if (pixel) + { + if (run_length >= 2) + count++; + run_length = 0; + } + else + { + run_length++; + } + + a += n; + *dom += dom_step; + if (a >= d) + { + *nondom += nondom_step; + a -= d; + } + } + + return count; +} + +/* Try the measure the timing pattern for a given QR code. This does + * not require the global perspective to have been set up, but it + * does require that the capstone corners have been set to their + * canonical rotation. + * + * For each capstone, we find a point in the middle of the ring band + * which is nearest the centre of the code. Using these points, we do + * a horizontal and a vertical timing scan. + */ +static int measure_timing_pattern(struct quirc *q, int index) +{ + struct quirc_grid *qr = &q->grids[index]; + int i; + int scan; + int ver; + int size; + + for (i = 0; i < 3; i++) + { + static const float us[] = {6.5, 6.5, 0.5}; + static const float vs[] = {0.5, 6.5, 6.5}; + struct quirc_capstone *cap = &q->capstones[qr->caps[i]]; + + perspective_map(cap->c, us[i], vs[i], &qr->tpep[i]); + } + + qr->hscan = timing_scan(q, &qr->tpep[1], &qr->tpep[2]); + qr->vscan = timing_scan(q, &qr->tpep[1], &qr->tpep[0]); + + scan = qr->hscan; + if (qr->vscan > scan) + scan = qr->vscan; + + /* If neither scan worked, we can't go any further. */ + if (scan < 0) + return -1; + + /* Choose the nearest allowable grid size */ + size = scan * 2 + 13; + ver = (size - 15) / 4; + qr->grid_size = ver * 4 + 17; + + return 0; +} + +/* Read a cell from a grid using the currently set perspective + * transform. Returns +/- 1 for black/white, 0 for cells which are + * out of image bounds. + */ +static int read_cell(const struct quirc *q, int index, int x, int y) +{ + const struct quirc_grid *qr = &q->grids[index]; + struct quirc_point p; + + perspective_map(qr->c, x + 0.5, y + 0.5, &p); + if (p.y < 0 || p.y >= q->h || p.x < 0 || p.x >= q->w) + return 0; + + return q->pixels[p.y * q->w + p.x] ? 1 : -1; +} + +static int fitness_cell(const struct quirc *q, int index, int x, int y) +{ + const struct quirc_grid *qr = &q->grids[index]; + int score = 0; + int u, v; + + for (v = 0; v < 3; v++) + for (u = 0; u < 3; u++) + { + static const float offsets[] = {0.3, 0.5, 0.7}; + struct quirc_point p; + + perspective_map(qr->c, x + offsets[u], + y + offsets[v], &p); + if (p.y < 0 || p.y >= q->h || p.x < 0 || p.x >= q->w) + continue; + + if (q->pixels[p.y * q->w + p.x]) + score++; + else + score--; + } + + return score; +} + +static int fitness_ring(const struct quirc *q, int index, int cx, int cy, + int radius) +{ + int i; + int score = 0; + + for (i = 0; i < radius * 2; i++) + { + score += fitness_cell(q, index, cx - radius + i, cy - radius); + score += fitness_cell(q, index, cx - radius, cy + radius - i); + score += fitness_cell(q, index, cx + radius, cy - radius + i); + score += fitness_cell(q, index, cx + radius - i, cy + radius); + } + + return score; +} + +static int fitness_apat(const struct quirc *q, int index, int cx, int cy) +{ + return fitness_cell(q, index, cx, cy) - + fitness_ring(q, index, cx, cy, 1) + + fitness_ring(q, index, cx, cy, 2); +} + +static int fitness_capstone(const struct quirc *q, int index, int x, int y) +{ + x += 3; + y += 3; + + return fitness_cell(q, index, x, y) + + fitness_ring(q, index, x, y, 1) - + fitness_ring(q, index, x, y, 2) + + fitness_ring(q, index, x, y, 3); +} + +/* Compute a fitness score for the currently configured perspective + * transform, using the features we expect to find by scanning the + * grid. + */ +static int fitness_all(const struct quirc *q, int index) +{ + const struct quirc_grid *qr = &q->grids[index]; + int version = (qr->grid_size - 17) / 4; + const struct quirc_version_info *info = &quirc_version_db[version]; + int score = 0; + int i, j; + int ap_count; + + /* Check the timing pattern */ + for (i = 0; i < qr->grid_size - 14; i++) + { + int expect = (i & 1) ? 1 : -1; + + score += fitness_cell(q, index, i + 7, 6) * expect; + score += fitness_cell(q, index, 6, i + 7) * expect; + } + + /* Check capstones */ + score += fitness_capstone(q, index, 0, 0); + score += fitness_capstone(q, index, qr->grid_size - 7, 0); + score += fitness_capstone(q, index, 0, qr->grid_size - 7); + + if (version < 0 || version > QUIRC_MAX_VERSION) + return score; + + /* Check alignment patterns */ + ap_count = 0; + while ((ap_count < QUIRC_MAX_ALIGNMENT) && info->apat[ap_count]) + ap_count++; + + for (i = 1; i + 1 < ap_count; i++) + { + score += fitness_apat(q, index, 6, info->apat[i]); + score += fitness_apat(q, index, info->apat[i], 6); + } + + for (i = 1; i < ap_count; i++) + for (j = 1; j < ap_count; j++) + score += fitness_apat(q, index, + info->apat[i], info->apat[j]); + //mp_printf(&mp_plat_print, "##score=%d\n",score); + return score; +} + +static void jiggle_perspective(struct quirc *q, int index) +{ + struct quirc_grid *qr = &q->grids[index]; + int best = fitness_all(q, index); + int pass; + float adjustments[8]; + int i; + + for (i = 0; i < 8; i++) + adjustments[i] = qr->c[i] * 0.02; + + for (pass = 0; pass < 5; pass++) + { + for (i = 0; i < 16; i++) + { + int j = i >> 1; + int test; + float old = qr->c[j]; + float step = adjustments[j]; + float new; + + if (i & 1) + new = old + step; + else + new = old - step; + + qr->c[j] = new; + test = fitness_all(q, index); + + if (test > best) + best = test; + else + qr->c[j] = old; + } + + for (i = 0; i < 8; i++) + adjustments[i] *= 0.5; + } +} + +/* Once the capstones are in place and an alignment point has been + * chosen, we call this function to set up a grid-reading perspective + * transform. + */ +static void setup_qr_perspective(struct quirc *q, int index) +{ + struct quirc_grid *qr = &q->grids[index]; + struct quirc_point rect[4]; + + /* Set up the perspective map for reading the grid */ + memcpy(&rect[0], &q->capstones[qr->caps[1]].corners[0], + sizeof(rect[0])); + memcpy(&rect[1], &q->capstones[qr->caps[2]].corners[0], + sizeof(rect[0])); + memcpy(&rect[2], &qr->align, sizeof(rect[0])); + memcpy(&rect[3], &q->capstones[qr->caps[0]].corners[0], + sizeof(rect[0])); + perspective_setup(qr->c, rect, qr->grid_size - 7, qr->grid_size - 7); + + jiggle_perspective(q, index); +} + +/* Rotate the capstone with so that corner 0 is the leftmost with respect + * to the given reference line. + */ +static void rotate_capstone(struct quirc_capstone *cap, + const struct quirc_point *h0, + const struct quirc_point *hd) +{ + struct quirc_point copy[4]; + int j; + int best = 0; + int best_score = 0; + + for (j = 0; j < 4; j++) + { + struct quirc_point *p = &cap->corners[j]; + int score = (p->x - h0->x) * -hd->y + + (p->y - h0->y) * hd->x; + + if (!j || score < best_score) + { + best = j; + best_score = score; + } + } + + /* Rotate the capstone */ + for (j = 0; j < 4; j++) + memcpy(©[j], &cap->corners[(j + best) % 4], + sizeof(copy[j])); + memcpy(cap->corners, copy, sizeof(cap->corners)); + perspective_setup(cap->c, cap->corners, 7.0, 7.0); +} + +static void record_qr_grid(struct quirc *q, int a, int b, int c) +{ + struct quirc_point h0, hd; + int i; + int qr_index; + struct quirc_grid *qr; + + if (q->num_grids >= QUIRC_MAX_GRIDS) + return; + + /* Construct the hypotenuse line from A to C. B should be to + * the left of this line. + */ + memcpy(&h0, &q->capstones[a].center, sizeof(h0)); + hd.x = q->capstones[c].center.x - q->capstones[a].center.x; + hd.y = q->capstones[c].center.y - q->capstones[a].center.y; + + /* Make sure A-B-C is clockwise */ + if ((q->capstones[b].center.x - h0.x) * -hd.y + + (q->capstones[b].center.y - h0.y) * hd.x > + 0) + { + int swap = a; + + a = c; + c = swap; + hd.x = -hd.x; + hd.y = -hd.y; + } + + /* Record the grid and its components */ + qr_index = q->num_grids; + qr = &q->grids[q->num_grids++]; + + memset(qr, 0, sizeof(*qr)); + qr->caps[0] = a; + qr->caps[1] = b; + qr->caps[2] = c; + qr->align_region = -1; + + /* Rotate each capstone so that corner 0 is top-left with respect + * to the grid. + */ + for (i = 0; i < 3; i++) + { + struct quirc_capstone *cap = &q->capstones[qr->caps[i]]; + + rotate_capstone(cap, &h0, &hd); + cap->qr_grid = qr_index; + } + + /* Check the timing pattern. This doesn't require a perspective + * transform. + */ + if (measure_timing_pattern(q, qr_index) < 0) + goto fail; + + /* Make an estimate based for the alignment pattern based on extending + * lines from capstones A and C. + */ + if (!line_intersect(&q->capstones[a].corners[0], + &q->capstones[a].corners[1], + &q->capstones[c].corners[0], + &q->capstones[c].corners[3], + &qr->align)) + goto fail; + + /* On V2+ grids, we should use the alignment pattern. */ + if (qr->grid_size > 21) + { + /* Try to find the actual location of the alignment pattern. */ + find_alignment_pattern(q, qr_index); + + /* Find the point of the alignment pattern closest to the + * top-left of the QR grid. + */ + if (qr->align_region >= 0) + { + struct polygon_score_data psd; + struct quirc_region *reg = + &q->regions[qr->align_region]; + + /* Start from some point inside the alignment pattern */ + memcpy(&qr->align, ®->seed, sizeof(qr->align)); + + memcpy(&psd.ref, &hd, sizeof(psd.ref)); + psd.corners = &qr->align; + psd.scores[0] = -hd.y * qr->align.x + + hd.x * qr->align.y; + + flood_fill_seed(q, reg->seed.x, reg->seed.y, + qr->align_region, QUIRC_PIXEL_BLACK, + NULL, NULL, 0); + flood_fill_seed(q, reg->seed.x, reg->seed.y, + QUIRC_PIXEL_BLACK, qr->align_region, + find_leftmost_to_line, &psd, 0); + } + } + + setup_qr_perspective(q, qr_index); + return; + +fail: + /* We've been unable to complete setup for this grid. Undo what we've + * recorded and pretend it never happened. + */ + for (i = 0; i < 3; i++) + q->capstones[qr->caps[i]].qr_grid = -1; + q->num_grids--; +} + +struct neighbour +{ + int index; + float distance; +} __attribute__((aligned(8))); + +struct neighbour_list +{ + struct neighbour n[QUIRC_MAX_CAPSTONES]; + int count; +} __attribute__((aligned(8))); + +static void test_neighbours(struct quirc *q, int i, + const struct neighbour_list *hlist, + const struct neighbour_list *vlist) +{ + int j, k; + float best_score = 0.0; + int best_h = -1, best_v = -1; + + /* Test each possible grouping */ + for (j = 0; j < hlist->count; j++) + for (k = 0; k < vlist->count; k++) + { + const struct neighbour *hn = &hlist->n[j]; + const struct neighbour *vn = &vlist->n[k]; + float score = fast_fabsf(1.0 - hn->distance / vn->distance); + + if (score > 2.5) + continue; + + if (best_h < 0 || score < best_score) + { + best_h = hn->index; + best_v = vn->index; + best_score = score; + } + } + + if (best_h < 0 || best_v < 0) + return; + + record_qr_grid(q, best_h, i, best_v); +} + +static void test_grouping(struct quirc *q, int i) +{ + struct quirc_capstone *c1 = &q->capstones[i]; + int j; + struct neighbour_list hlist; + struct neighbour_list vlist; + + if (c1->qr_grid >= 0) + return; + + hlist.count = 0; + vlist.count = 0; + + /* Look for potential neighbours by examining the relative gradients + * from this capstone to others. + */ + for (j = 0; j < q->num_capstones; j++) + { + struct quirc_capstone *c2 = &q->capstones[j]; + float u, v; + + if (i == j || c2->qr_grid >= 0) + continue; + + perspective_unmap(c1->c, &c2->center, &u, &v); + + u = fast_fabsf(u - 3.5); + v = fast_fabsf(v - 3.5); + + if (u < 0.2 * v) + { + struct neighbour *n = &hlist.n[hlist.count++]; + + n->index = j; + n->distance = v; + } + + if (v < 0.2 * u) + { + struct neighbour *n = &vlist.n[vlist.count++]; + + n->index = j; + n->distance = u; + } + } + + if (!(hlist.count && vlist.count)) + return; + + test_neighbours(q, i, &hlist, &vlist); +} + +static void pixels_setup(struct quirc *q) +{ + if (sizeof(*q->image) == sizeof(*q->pixels)) + { + q->pixels = (quirc_pixel_t *)q->image; + } + else + { + int x, y; + for (y = 0; y < q->h; y++) + { + for (x = 0; x < q->w; x++) + { + q->pixels[y * q->w + x] = q->image[y * q->w + x]; + } + } + } +} + +uint8_t *quirc_begin(struct quirc *q, int *w, int *h) +{ + q->num_regions = QUIRC_PIXEL_REGION; + q->num_capstones = 0; + q->num_grids = 0; + + if (w) + *w = q->w; + if (h) + *h = q->h; + + return q->image; +} + +void quirc_end(struct quirc *q) +{ + int i; + pixels_setup(q); + threshold(q); + + for (i = 0; i < q->h; i++) + { + finder_scan(q, i); + } + + for (i = 0; i < q->num_capstones; i++) + { + test_grouping(q, i); + } +} + +void quirc_extract(const struct quirc *q, int index, + struct quirc_code *code) +{ + const struct quirc_grid *qr = &q->grids[index]; + int y; + int i = 0; + + if (index < 0 || index > q->num_grids) + return; + + memset(code, 0, sizeof(*code)); + + perspective_map(qr->c, 0.0, 0.0, &code->corners[0]); + perspective_map(qr->c, qr->grid_size, 0.0, &code->corners[1]); + perspective_map(qr->c, qr->grid_size, qr->grid_size, + &code->corners[2]); + perspective_map(qr->c, 0.0, qr->grid_size, &code->corners[3]); + + code->size = qr->grid_size; + + for (y = 0; y < qr->grid_size; y++) + { + int x; + + for (x = 0; x < qr->grid_size; x++) + { + if (read_cell(q, index, x, y) > 0) + code->cell_bitmap[i >> 3] |= (1 << (i & 7)); + + i++; + } + } +} \ No newline at end of file diff --git a/lib/ESP32QRCodeReader_vendor/src/quirc/quirc.c b/lib/ESP32QRCodeReader_vendor/src/quirc/quirc.c new file mode 100644 index 0000000..e5a30fb --- /dev/null +++ b/lib/ESP32QRCodeReader_vendor/src/quirc/quirc.c @@ -0,0 +1,104 @@ +/* quirc -- QR-code recognition library + * Copyright (C) 2010-2012 Daniel Beer + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include "quirc_internal.h" +#include + +const char *quirc_version(void) +{ + return "1.0"; +} + +//static struct quirc _q; +struct quirc *quirc_new(void) +{ + struct quirc *q = ps_malloc(sizeof(*q)); + + if (!q) + return NULL; + + memset(q, 0, sizeof(*q)); + return q; +} + +void quirc_destroy(struct quirc *q) +{ + if (q->image) + if (q->image) + free(q->image); + if (sizeof(*q->image) != sizeof(*q->pixels)) + if (q->pixels) + free(q->pixels); + + if (q) + free(q); +} + +//static quirc_pixel_t img_buf[320*240]; +int quirc_resize(struct quirc *q, int w, int h) +{ + if (q->image) + { + free(q->image); + } + uint8_t *new_image = ps_malloc(w * h); + + if (!new_image) + return -1; + + if (sizeof(*q->image) != sizeof(*q->pixels)) + { //should gray, 1==1 + size_t new_size = w * h * sizeof(quirc_pixel_t); + if (q->pixels) + free(q->pixels); + quirc_pixel_t *new_pixels = ps_malloc(new_size); + if (!new_pixels) + { + free(new_image); + return -1; + } + q->pixels = new_pixels; + } + q->image = new_image; + q->w = w; + q->h = h; + return 0; +} + +int quirc_count(const struct quirc *q) +{ + return q->num_grids; +} + +static const char *const error_table[] = { + [QUIRC_SUCCESS] = "Success", + [QUIRC_ERROR_INVALID_GRID_SIZE] = "Invalid grid size", + [QUIRC_ERROR_INVALID_VERSION] = "Invalid version", + [QUIRC_ERROR_FORMAT_ECC] = "Format data ECC failure", + [QUIRC_ERROR_DATA_ECC] = "ECC failure", + [QUIRC_ERROR_UNKNOWN_DATA_TYPE] = "Unknown data type", + [QUIRC_ERROR_DATA_OVERFLOW] = "Data overflow", + [QUIRC_ERROR_DATA_UNDERFLOW] = "Data underflow"}; + +const char *quirc_strerror(quirc_decode_error_t err) +{ + if (err >= 0 && err < sizeof(error_table) / sizeof(error_table[0])) + return error_table[err]; + + return "Unknown error"; +} diff --git a/lib/ESP32QRCodeReader_vendor/src/quirc/quirc.h b/lib/ESP32QRCodeReader_vendor/src/quirc/quirc.h new file mode 100644 index 0000000..ecdc172 --- /dev/null +++ b/lib/ESP32QRCodeReader_vendor/src/quirc/quirc.h @@ -0,0 +1,178 @@ +/* quirc -- QR-code recognition library + * Copyright (C) 2010-2012 Daniel Beer + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef QUIRC_H_ +#define QUIRC_H_ + +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + + struct quirc; + + /* Obtain the library version string. */ + const char *quirc_version(void); + + /* Construct a new QR-code recognizer. This function will return NULL + * if sufficient memory could not be allocated. + */ + struct quirc *quirc_new(void); + + /* Destroy a QR-code recognizer. */ + void quirc_destroy(struct quirc *q); + + /* Resize the QR-code recognizer. The size of an image must be + * specified before codes can be analyzed. + * + * This function returns 0 on success, or -1 if sufficient memory could + * not be allocated. + */ + int quirc_resize(struct quirc *q, int w, int h); + + /* These functions are used to process images for QR-code recognition. + * quirc_begin() must first be called to obtain access to a buffer into + * which the input image should be placed. Optionally, the current + * width and height may be returned. + * + * After filling the buffer, quirc_end() should be called to process + * the image for QR-code recognition. The locations and content of each + * code may be obtained using accessor functions described below. + */ + uint8_t *quirc_begin(struct quirc *q, int *w, int *h); + void quirc_end(struct quirc *q); + + /* This structure describes a location in the input image buffer. */ + struct quirc_point + { + int x; + int y; + } __attribute__((aligned(8))); + + /* This enum describes the various decoder errors which may occur. */ + typedef enum + { + QUIRC_SUCCESS = 0, + QUIRC_ERROR_INVALID_GRID_SIZE, + QUIRC_ERROR_INVALID_VERSION, + QUIRC_ERROR_FORMAT_ECC, + QUIRC_ERROR_DATA_ECC, + QUIRC_ERROR_UNKNOWN_DATA_TYPE, + QUIRC_ERROR_DATA_OVERFLOW, + QUIRC_ERROR_DATA_UNDERFLOW + } quirc_decode_error_t; + + /* Return a string error message for an error code. */ + const char *quirc_strerror(quirc_decode_error_t err); + +/* Limits on the maximum size of QR-codes and their content. */ +#define QUIRC_MAX_BITMAP 3917 +#define QUIRC_MAX_PAYLOAD 8896 + +/* QR-code ECC types. */ +#define QUIRC_ECC_LEVEL_M 0 +#define QUIRC_ECC_LEVEL_L 1 +#define QUIRC_ECC_LEVEL_H 2 +#define QUIRC_ECC_LEVEL_Q 3 + +/* QR-code data types. */ +#define QUIRC_DATA_TYPE_NUMERIC 1 +#define QUIRC_DATA_TYPE_ALPHA 2 +#define QUIRC_DATA_TYPE_BYTE 4 +#define QUIRC_DATA_TYPE_KANJI 8 + +/* Common character encodings */ +#define QUIRC_ECI_ISO_8859_1 1 +#define QUIRC_ECI_IBM437 2 +#define QUIRC_ECI_ISO_8859_2 4 +#define QUIRC_ECI_ISO_8859_3 5 +#define QUIRC_ECI_ISO_8859_4 6 +#define QUIRC_ECI_ISO_8859_5 7 +#define QUIRC_ECI_ISO_8859_6 8 +#define QUIRC_ECI_ISO_8859_7 9 +#define QUIRC_ECI_ISO_8859_8 10 +#define QUIRC_ECI_ISO_8859_9 11 +#define QUIRC_ECI_WINDOWS_874 13 +#define QUIRC_ECI_ISO_8859_13 15 +#define QUIRC_ECI_ISO_8859_15 17 +#define QUIRC_ECI_SHIFT_JIS 20 +#define QUIRC_ECI_UTF_8 26 + + /* This structure is used to return information about detected QR codes + * in the input image. + */ + struct quirc_code + { + /* The four corners of the QR-code, from top left, clockwise */ + struct quirc_point corners[4]; + + /* The number of cells across in the QR-code. The cell bitmap + * is a bitmask giving the actual values of cells. If the cell + * at (x, y) is black, then the following bit is set: + * + * cell_bitmap[i >> 3] & (1 << (i & 7)) + * + * where i = (y * size) + x. + */ + int size; + uint8_t cell_bitmap[QUIRC_MAX_BITMAP]; + } __attribute__((aligned(8))); + + /* This structure holds the decoded QR-code data */ + struct quirc_data + { + /* Various parameters of the QR-code. These can mostly be + * ignored if you only care about the data. + */ + int version; + int ecc_level; + int mask; + + /* This field is the highest-valued data type found in the QR + * code. + */ + int data_type; + + /* Data payload. For the Kanji datatype, payload is encoded as + * Shift-JIS. For all other datatypes, payload is ASCII text. + */ + uint8_t payload[QUIRC_MAX_PAYLOAD]; + int payload_len; + + /* ECI assignment number */ + uint32_t eci; + } __attribute__((aligned(8))); + + /* Return the number of QR-codes identified in the last processed + * image. + */ + int quirc_count(const struct quirc *q); + + /* Extract the QR-code specified by the given index. */ + void quirc_extract(const struct quirc *q, int index, + struct quirc_code *code); + + /* Decode a QR-code, returning the payload data. */ + quirc_decode_error_t quirc_decode(const struct quirc_code *code, + struct quirc_data *data); + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/lib/ESP32QRCodeReader_vendor/src/quirc/quirc_internal.h b/lib/ESP32QRCodeReader_vendor/src/quirc/quirc_internal.h new file mode 100644 index 0000000..3fa075c --- /dev/null +++ b/lib/ESP32QRCodeReader_vendor/src/quirc/quirc_internal.h @@ -0,0 +1,121 @@ +/* quirc -- QR-code recognition library + * Copyright (C) 2010-2012 Daniel Beer + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef QUIRC_INTERNAL_H_ +#define QUIRC_INTERNAL_H_ + +#include "quirc.h" + +#define QUIRC_PIXEL_WHITE 0 +#define QUIRC_PIXEL_BLACK 1 +#define QUIRC_PIXEL_REGION 2 + +#ifndef QUIRC_MAX_REGIONS +#define QUIRC_MAX_REGIONS 254 +#endif + +#define QUIRC_MAX_CAPSTONES 32 +#define QUIRC_MAX_GRIDS 8 + +#define QUIRC_PERSPECTIVE_PARAMS 8 + +#if QUIRC_MAX_REGIONS < UINT8_MAX +typedef uint8_t quirc_pixel_t; +#elif QUIRC_MAX_REGIONS < UINT16_MAX +typedef uint16_t quirc_pixel_t; +#else +#error "QUIRC_MAX_REGIONS > 65534 is not supported" +#endif + +struct quirc_region +{ + struct quirc_point seed; + int count; + int capstone; +} __attribute__((aligned(8))); + +struct quirc_capstone +{ + int ring; + int stone; + + struct quirc_point corners[4]; + struct quirc_point center; + float c[QUIRC_PERSPECTIVE_PARAMS]; + + int qr_grid; +} __attribute__((aligned(8))); + +struct quirc_grid +{ + /* Capstone indices */ + int caps[3]; + + /* Alignment pattern region and corner */ + int align_region; + struct quirc_point align; + + /* Timing pattern endpoints */ + struct quirc_point tpep[3]; + int hscan; + int vscan; + + /* Grid size and perspective transform */ + int grid_size; + float c[QUIRC_PERSPECTIVE_PARAMS]; +} __attribute__((aligned(8))); + +struct quirc +{ + uint8_t *image; + quirc_pixel_t *pixels; + int w; + int h; + + int num_regions; + struct quirc_region regions[QUIRC_MAX_REGIONS]; + + int num_capstones; + struct quirc_capstone capstones[QUIRC_MAX_CAPSTONES]; + + int num_grids; + struct quirc_grid grids[QUIRC_MAX_GRIDS]; +} __attribute__((aligned(8))); + +/************************************************************************ + * QR-code version information database + */ + +#define QUIRC_MAX_VERSION 40 +#define QUIRC_MAX_ALIGNMENT 7 + +struct quirc_rs_params +{ + uint8_t bs; /* Small block size */ + uint8_t dw; /* Small data words */ + uint8_t ns; /* Number of small blocks */ +} __attribute__((aligned(8))); + +struct quirc_version_info +{ + uint16_t data_bytes; + uint8_t apat[QUIRC_MAX_ALIGNMENT]; + struct quirc_rs_params ecc[4]; +} __attribute__((aligned(8))); + +extern const struct quirc_version_info quirc_version_db[QUIRC_MAX_VERSION + 1]; + +#endif \ No newline at end of file diff --git a/lib/ESP32QRCodeReader_vendor/src/quirc/version_db.c b/lib/ESP32QRCodeReader_vendor/src/quirc/version_db.c new file mode 100644 index 0000000..0da2d07 --- /dev/null +++ b/lib/ESP32QRCodeReader_vendor/src/quirc/version_db.c @@ -0,0 +1,184 @@ +/* quirc -- QR-code recognition library + * Copyright (C) 2010-2012 Daniel Beer + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "quirc_internal.h" + +const struct quirc_version_info quirc_version_db[QUIRC_MAX_VERSION + 1] = { + {0}, + {/* Version 1 */ + .data_bytes = 26, + .apat = {0}, + .ecc = { + {.bs = 26, .dw = 16, .ns = 1}, + {.bs = 26, .dw = 19, .ns = 1}, + {.bs = 26, .dw = 9, .ns = 1}, + {.bs = 26, .dw = 13, .ns = 1}}}, + {/* Version 2 */ + .data_bytes = 44, + .apat = {6, 18, 0}, + .ecc = {{.bs = 44, .dw = 28, .ns = 1}, {.bs = 44, .dw = 34, .ns = 1}, {.bs = 44, .dw = 16, .ns = 1}, {.bs = 44, .dw = 22, .ns = 1}}}, + {/* Version 3 */ + .data_bytes = 70, + .apat = {6, 22, 0}, + .ecc = {{.bs = 70, .dw = 44, .ns = 1}, {.bs = 70, .dw = 55, .ns = 1}, {.bs = 35, .dw = 13, .ns = 2}, {.bs = 35, .dw = 17, .ns = 2}}}, + {/* Version 4 */ + .data_bytes = 100, + .apat = {6, 26, 0}, + .ecc = {{.bs = 50, .dw = 32, .ns = 2}, {.bs = 100, .dw = 80, .ns = 1}, {.bs = 25, .dw = 9, .ns = 4}, {.bs = 50, .dw = 24, .ns = 2}}}, + {/* Version 5 */ + .data_bytes = 134, + .apat = {6, 30, 0}, + .ecc = {{.bs = 67, .dw = 43, .ns = 2}, {.bs = 134, .dw = 108, .ns = 1}, {.bs = 33, .dw = 11, .ns = 2}, {.bs = 33, .dw = 15, .ns = 2}}}, + {/* Version 6 */ + .data_bytes = 172, + .apat = {6, 34, 0}, + .ecc = {{.bs = 43, .dw = 27, .ns = 4}, {.bs = 86, .dw = 68, .ns = 2}, {.bs = 43, .dw = 15, .ns = 4}, {.bs = 43, .dw = 19, .ns = 4}}}, + {/* Version 7 */ + .data_bytes = 196, + .apat = {6, 22, 38, 0}, + .ecc = {{.bs = 49, .dw = 31, .ns = 4}, {.bs = 98, .dw = 78, .ns = 2}, {.bs = 39, .dw = 13, .ns = 4}, {.bs = 32, .dw = 14, .ns = 2}}}, + {/* Version 8 */ + .data_bytes = 242, + .apat = {6, 24, 42, 0}, + .ecc = {{.bs = 60, .dw = 38, .ns = 2}, {.bs = 121, .dw = 97, .ns = 2}, {.bs = 40, .dw = 14, .ns = 4}, {.bs = 40, .dw = 18, .ns = 4}}}, + {/* Version 9 */ + .data_bytes = 292, + .apat = {6, 26, 46, 0}, + .ecc = {{.bs = 58, .dw = 36, .ns = 3}, {.bs = 146, .dw = 116, .ns = 2}, {.bs = 36, .dw = 12, .ns = 4}, {.bs = 36, .dw = 16, .ns = 4}}}, + {/* Version 10 */ + .data_bytes = 346, + .apat = {6, 28, 50, 0}, + .ecc = {{.bs = 69, .dw = 43, .ns = 4}, {.bs = 86, .dw = 68, .ns = 2}, {.bs = 43, .dw = 15, .ns = 6}, {.bs = 43, .dw = 19, .ns = 6}}}, + {/* Version 11 */ + .data_bytes = 404, + .apat = {6, 30, 54, 0}, + .ecc = {{.bs = 80, .dw = 50, .ns = 1}, {.bs = 101, .dw = 81, .ns = 4}, {.bs = 36, .dw = 12, .ns = 3}, {.bs = 50, .dw = 22, .ns = 4}}}, + {/* Version 12 */ + .data_bytes = 466, + .apat = {6, 32, 58, 0}, + .ecc = {{.bs = 58, .dw = 36, .ns = 6}, {.bs = 116, .dw = 92, .ns = 2}, {.bs = 42, .dw = 14, .ns = 7}, {.bs = 46, .dw = 20, .ns = 4}}}, + {/* Version 13 */ + .data_bytes = 532, + .apat = {6, 34, 62, 0}, + .ecc = {{.bs = 59, .dw = 37, .ns = 8}, {.bs = 133, .dw = 107, .ns = 4}, {.bs = 33, .dw = 11, .ns = 12}, {.bs = 44, .dw = 20, .ns = 8}}}, + {/* Version 14 */ + .data_bytes = 581, + .apat = {6, 26, 46, 66, 0}, + .ecc = {{.bs = 64, .dw = 40, .ns = 4}, {.bs = 145, .dw = 115, .ns = 3}, {.bs = 36, .dw = 12, .ns = 11}, {.bs = 36, .dw = 16, .ns = 11}}}, + {/* Version 15 */ + .data_bytes = 655, + .apat = {6, 26, 48, 70, 0}, + .ecc = {{.bs = 65, .dw = 41, .ns = 5}, {.bs = 109, .dw = 87, .ns = 5}, {.bs = 36, .dw = 12, .ns = 11}, {.bs = 54, .dw = 24, .ns = 5}}}, + {/* Version 16 */ + .data_bytes = 733, + .apat = {6, 26, 50, 74, 0}, + .ecc = {{.bs = 73, .dw = 45, .ns = 7}, {.bs = 122, .dw = 98, .ns = 5}, {.bs = 45, .dw = 15, .ns = 3}, {.bs = 43, .dw = 19, .ns = 15}}}, + {/* Version 17 */ + .data_bytes = 815, + .apat = {6, 30, 54, 78, 0}, + .ecc = {{.bs = 74, .dw = 46, .ns = 10}, {.bs = 135, .dw = 107, .ns = 1}, {.bs = 42, .dw = 14, .ns = 2}, {.bs = 50, .dw = 22, .ns = 1}}}, + {/* Version 18 */ + .data_bytes = 901, + .apat = {6, 30, 56, 82, 0}, + .ecc = {{.bs = 69, .dw = 43, .ns = 9}, {.bs = 150, .dw = 120, .ns = 5}, {.bs = 42, .dw = 14, .ns = 2}, {.bs = 50, .dw = 22, .ns = 17}}}, + {/* Version 19 */ + .data_bytes = 991, + .apat = {6, 30, 58, 86, 0}, + .ecc = {{.bs = 70, .dw = 44, .ns = 3}, {.bs = 141, .dw = 113, .ns = 3}, {.bs = 39, .dw = 13, .ns = 9}, {.bs = 47, .dw = 21, .ns = 17}}}, + {/* Version 20 */ + .data_bytes = 1085, + .apat = {6, 34, 62, 90, 0}, + .ecc = {{.bs = 67, .dw = 41, .ns = 3}, {.bs = 135, .dw = 107, .ns = 3}, {.bs = 43, .dw = 15, .ns = 15}, {.bs = 54, .dw = 24, .ns = 15}}}, + {/* Version 21 */ + .data_bytes = 1156, + .apat = {6, 28, 50, 72, 92, 0}, + .ecc = {{.bs = 68, .dw = 42, .ns = 17}, {.bs = 144, .dw = 116, .ns = 4}, {.bs = 46, .dw = 16, .ns = 19}, {.bs = 50, .dw = 22, .ns = 17}}}, + {/* Version 22 */ + .data_bytes = 1258, + .apat = {6, 26, 50, 74, 98, 0}, + .ecc = {{.bs = 74, .dw = 46, .ns = 17}, {.bs = 139, .dw = 111, .ns = 2}, {.bs = 37, .dw = 13, .ns = 34}, {.bs = 54, .dw = 24, .ns = 7}}}, + {/* Version 23 */ + .data_bytes = 1364, + .apat = {6, 30, 54, 78, 102, 0}, + .ecc = {{.bs = 75, .dw = 47, .ns = 4}, {.bs = 151, .dw = 121, .ns = 4}, {.bs = 45, .dw = 15, .ns = 16}, {.bs = 54, .dw = 24, .ns = 11}}}, + {/* Version 24 */ + .data_bytes = 1474, + .apat = {6, 28, 54, 80, 106, 0}, + .ecc = {{.bs = 73, .dw = 45, .ns = 6}, {.bs = 147, .dw = 117, .ns = 6}, {.bs = 46, .dw = 16, .ns = 30}, {.bs = 54, .dw = 24, .ns = 11}}}, + {/* Version 25 */ + .data_bytes = 1588, + .apat = {6, 32, 58, 84, 110, 0}, + .ecc = {{.bs = 75, .dw = 47, .ns = 8}, {.bs = 132, .dw = 106, .ns = 8}, {.bs = 45, .dw = 15, .ns = 22}, {.bs = 54, .dw = 24, .ns = 7}}}, + {/* Version 26 */ + .data_bytes = 1706, + .apat = {6, 30, 58, 86, 114, 0}, + .ecc = {{.bs = 74, .dw = 46, .ns = 19}, {.bs = 142, .dw = 114, .ns = 10}, {.bs = 46, .dw = 16, .ns = 33}, {.bs = 50, .dw = 22, .ns = 28}}}, + {/* Version 27 */ + .data_bytes = 1828, + .apat = {6, 34, 62, 90, 118, 0}, + .ecc = {{.bs = 73, .dw = 45, .ns = 22}, {.bs = 152, .dw = 122, .ns = 8}, {.bs = 45, .dw = 15, .ns = 12}, {.bs = 53, .dw = 23, .ns = 8}}}, + {/* Version 28 */ + .data_bytes = 1921, + .apat = {6, 26, 50, 74, 98, 122, 0}, + .ecc = {{.bs = 73, .dw = 45, .ns = 3}, {.bs = 147, .dw = 117, .ns = 3}, {.bs = 45, .dw = 15, .ns = 11}, {.bs = 54, .dw = 24, .ns = 4}}}, + {/* Version 29 */ + .data_bytes = 2051, + .apat = {6, 30, 54, 78, 102, 126, 0}, + .ecc = {{.bs = 73, .dw = 45, .ns = 21}, {.bs = 146, .dw = 116, .ns = 7}, {.bs = 45, .dw = 15, .ns = 19}, {.bs = 53, .dw = 23, .ns = 1}}}, + {/* Version 30 */ + .data_bytes = 2185, + .apat = {6, 26, 52, 78, 104, 130, 0}, + .ecc = {{.bs = 75, .dw = 47, .ns = 19}, {.bs = 145, .dw = 115, .ns = 5}, {.bs = 45, .dw = 15, .ns = 23}, {.bs = 54, .dw = 24, .ns = 15}}}, + {/* Version 31 */ + .data_bytes = 2323, + .apat = {6, 30, 56, 82, 108, 134, 0}, + .ecc = {{.bs = 74, .dw = 46, .ns = 2}, {.bs = 145, .dw = 115, .ns = 13}, {.bs = 45, .dw = 15, .ns = 23}, {.bs = 54, .dw = 24, .ns = 42}}}, + {/* Version 32 */ + .data_bytes = 2465, + .apat = {6, 34, 60, 86, 112, 138, 0}, + .ecc = {{.bs = 74, .dw = 46, .ns = 10}, {.bs = 145, .dw = 115, .ns = 17}, {.bs = 45, .dw = 15, .ns = 19}, {.bs = 54, .dw = 24, .ns = 10}}}, + {/* Version 33 */ + .data_bytes = 2611, + .apat = {6, 30, 58, 86, 114, 142, 0}, + .ecc = {{.bs = 74, .dw = 46, .ns = 14}, {.bs = 145, .dw = 115, .ns = 17}, {.bs = 45, .dw = 15, .ns = 11}, {.bs = 54, .dw = 24, .ns = 29}}}, + {/* Version 34 */ + .data_bytes = 2761, + .apat = {6, 34, 62, 90, 118, 146, 0}, + .ecc = {{.bs = 74, .dw = 46, .ns = 14}, {.bs = 145, .dw = 115, .ns = 13}, {.bs = 46, .dw = 16, .ns = 59}, {.bs = 54, .dw = 24, .ns = 44}}}, + {/* Version 35 */ + .data_bytes = 2876, + .apat = {6, 30, 54, 78, 102, 126, 150}, + .ecc = {{.bs = 75, .dw = 47, .ns = 12}, {.bs = 151, .dw = 121, .ns = 12}, {.bs = 45, .dw = 15, .ns = 22}, {.bs = 54, .dw = 24, .ns = 39}}}, + {/* Version 36 */ + .data_bytes = 3034, + .apat = {6, 24, 50, 76, 102, 128, 154}, + .ecc = {{.bs = 75, .dw = 47, .ns = 6}, {.bs = 151, .dw = 121, .ns = 6}, {.bs = 45, .dw = 15, .ns = 2}, {.bs = 54, .dw = 24, .ns = 46}}}, + {/* Version 37 */ + .data_bytes = 3196, + .apat = {6, 28, 54, 80, 106, 132, 158}, + .ecc = {{.bs = 74, .dw = 46, .ns = 29}, {.bs = 152, .dw = 122, .ns = 17}, {.bs = 45, .dw = 15, .ns = 24}, {.bs = 54, .dw = 24, .ns = 49}}}, + {/* Version 38 */ + .data_bytes = 3362, + .apat = {6, 32, 58, 84, 110, 136, 162}, + .ecc = {{.bs = 74, .dw = 46, .ns = 13}, {.bs = 152, .dw = 122, .ns = 4}, {.bs = 45, .dw = 15, .ns = 42}, {.bs = 54, .dw = 24, .ns = 48}}}, + {/* Version 39 */ + .data_bytes = 3532, + .apat = {6, 26, 54, 82, 110, 138, 166}, + .ecc = {{.bs = 75, .dw = 47, .ns = 40}, {.bs = 147, .dw = 117, .ns = 20}, {.bs = 45, .dw = 15, .ns = 10}, {.bs = 54, .dw = 24, .ns = 43}}}, + {/* Version 40 */ + .data_bytes = 3706, + .apat = {6, 30, 58, 86, 114, 142, 170}, + .ecc = {{.bs = 75, .dw = 47, .ns = 18}, {.bs = 148, .dw = 118, .ns = 19}, {.bs = 45, .dw = 15, .ns = 20}, {.bs = 54, .dw = 24, .ns = 34}}}}; diff --git a/lib/tinyexpr_vendor/LICENSE b/lib/tinyexpr_vendor/LICENSE new file mode 100644 index 0000000..3a1bc57 --- /dev/null +++ b/lib/tinyexpr_vendor/LICENSE @@ -0,0 +1,20 @@ +zlib License + +Copyright (C) 2015, 2016 Lewis Van Winkle + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgement in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + diff --git a/lib/tinyexpr_vendor/README.md b/lib/tinyexpr_vendor/README.md new file mode 100644 index 0000000..cf345d6 --- /dev/null +++ b/lib/tinyexpr_vendor/README.md @@ -0,0 +1,319 @@ +[![Build Status](https://travis-ci.com/codeplea/tinyexpr.svg?branch=master)](https://travis-ci.com/codeplea/tinyexpr) + + +TinyExpr logo + +# TinyExpr + +TinyExpr is a very small recursive descent parser and evaluation engine for +math expressions. It's handy when you want to add the ability to evaluate +math expressions at runtime without adding a bunch of cruft to your project. + +In addition to the standard math operators and precedence, TinyExpr also supports +the standard C math functions and runtime binding of variables. + +## Features + +- **C99 with no dependencies**. +- Single source file and header file. +- Simple and fast. +- Implements standard operators precedence. +- Exposes standard C math functions (sin, sqrt, ln, etc.). +- Can add custom functions and variables easily. +- Can bind variables at eval-time. +- Released under the zlib license - free for nearly any use. +- Easy to use and integrate with your code +- Thread-safe, provided that your *malloc* is. + +## Building + +TinyExpr is self-contained in two files: `tinyexpr.c` and `tinyexpr.h`. To use +TinyExpr, simply add those two files to your project. + +## Short Example + +Here is a minimal example to evaluate an expression at runtime. + +```C + #include "tinyexpr.h" + printf("%f\n", te_interp("5*5", 0)); /* Prints 25. */ +``` + + +## Usage + +TinyExpr defines only four functions: + +```C + double te_interp(const char *expression, int *error); + te_expr *te_compile(const char *expression, const te_variable *variables, int var_count, int *error); + double te_eval(const te_expr *expr); + void te_free(te_expr *expr); +``` + +## te_interp +```C + double te_interp(const char *expression, int *error); +``` + +`te_interp()` takes an expression and immediately returns the result of it. If there +is a parse error, `te_interp()` returns NaN. + +If the `error` pointer argument is not 0, then `te_interp()` will set `*error` to the position +of the parse error on failure, and set `*error` to 0 on success. + +**example usage:** + +```C + int error; + + double a = te_interp("(5+5)", 0); /* Returns 10. */ + double b = te_interp("(5+5)", &error); /* Returns 10, error is set to 0. */ + double c = te_interp("(5+5", &error); /* Returns NaN, error is set to 4. */ +``` + +## te_compile, te_eval, te_free +```C + te_expr *te_compile(const char *expression, const te_variable *lookup, int lookup_len, int *error); + double te_eval(const te_expr *n); + void te_free(te_expr *n); +``` + +Give `te_compile()` an expression with unbound variables and a list of +variable names and pointers. `te_compile()` will return a `te_expr*` which can +be evaluated later using `te_eval()`. On failure, `te_compile()` will return 0 +and optionally set the passed in `*error` to the location of the parse error. + +You may also compile expressions without variables by passing `te_compile()`'s second +and third arguments as 0. + +Give `te_eval()` a `te_expr*` from `te_compile()`. `te_eval()` will evaluate the expression +using the current variable values. + +After you're finished, make sure to call `te_free()`. + +**example usage:** + +```C + double x, y; + /* Store variable names and pointers. */ + te_variable vars[] = {{"x", &x}, {"y", &y}}; + + int err; + /* Compile the expression with variables. */ + te_expr *expr = te_compile("sqrt(x^2+y^2)", vars, 2, &err); + + if (expr) { + x = 3; y = 4; + const double h1 = te_eval(expr); /* Returns 5. */ + + x = 5; y = 12; + const double h2 = te_eval(expr); /* Returns 13. */ + + te_free(expr); + } else { + printf("Parse error at %d\n", err); + } + +``` + +## Longer Example + +Here is a complete example that will evaluate an expression passed in from the command +line. It also does error checking and binds the variables `x` and `y` to *3* and *4*, respectively. + +```C + #include "tinyexpr.h" + #include + + int main(int argc, char *argv[]) + { + if (argc < 2) { + printf("Usage: example2 \"expression\"\n"); + return 0; + } + + const char *expression = argv[1]; + printf("Evaluating:\n\t%s\n", expression); + + /* This shows an example where the variables + * x and y are bound at eval-time. */ + double x, y; + te_variable vars[] = {{"x", &x}, {"y", &y}}; + + /* This will compile the expression and check for errors. */ + int err; + te_expr *n = te_compile(expression, vars, 2, &err); + + if (n) { + /* The variables can be changed here, and eval can be called as many + * times as you like. This is fairly efficient because the parsing has + * already been done. */ + x = 3; y = 4; + const double r = te_eval(n); printf("Result:\n\t%f\n", r); + te_free(n); + } else { + /* Show the user where the error is at. */ + printf("\t%*s^\nError near here", err-1, ""); + } + + return 0; + } +``` + + +This produces the output: + + $ example2 "sqrt(x^2+y2)" + Evaluating: + sqrt(x^2+y2) + ^ + Error near here + + + $ example2 "sqrt(x^2+y^2)" + Evaluating: + sqrt(x^2+y^2) + Result: + 5.000000 + + +## Binding to Custom Functions + +TinyExpr can also call to custom functions implemented in C. Here is a short example: + +```C +double my_sum(double a, double b) { + /* Example C function that adds two numbers together. */ + return a + b; +} + +te_variable vars[] = { + {"mysum", my_sum, TE_FUNCTION2} /* TE_FUNCTION2 used because my_sum takes two arguments. */ +}; + +te_expr *n = te_compile("mysum(5, 6)", vars, 1, 0); + +``` + + +## How it works + +`te_compile()` uses a simple recursive descent parser to compile your +expression into a syntax tree. For example, the expression `"sin x + 1/4"` +parses as: + +![example syntax tree](doc/e1.png?raw=true) + +`te_compile()` also automatically prunes constant branches. In this example, +the compiled expression returned by `te_compile()` would become: + +![example syntax tree](doc/e2.png?raw=true) + +`te_eval()` will automatically load in any variables by their pointer, and then evaluate +and return the result of the expression. + +`te_free()` should always be called when you're done with the compiled expression. + + +## Speed + + +TinyExpr is pretty fast compared to C when the expression is short, when the +expression does hard calculations (e.g. exponentiation), and when some of the +work can be simplified by `te_compile()`. TinyExpr is slow compared to C when the +expression is long and involves only basic arithmetic. + +Here is some example performance numbers taken from the included +**benchmark.c** program: + +| Expression | te_eval time | native C time | slowdown | +| :------------- |-------------:| -----:|----:| +| sqrt(a^1.5+a^2.5) | 15,641 ms | 14,478 ms | 8% slower | +| a+5 | 765 ms | 563 ms | 36% slower | +| a+(5*2) | 765 ms | 563 ms | 36% slower | +| (a+5)*2 | 1422 ms | 563 ms | 153% slower | +| (1/(a+1)+2/(a+2)+3/(a+3)) | 5,516 ms | 1,266 ms | 336% slower | + + + +## Grammar + +TinyExpr parses the following grammar: + + = {"," } + = {("+" | "-") } + = {("*" | "/" | "%") } + = {"^" } + = {("-" | "+")} + = + | + | {"(" ")"} + | + | "(" {"," } ")" + | "(" ")" + +In addition, whitespace between tokens is ignored. + +Valid variable names consist of a letter followed by any combination of: +letters, the digits *0* through *9*, and underscore. Constants can be integers +or floating-point numbers, and can be in decimal, hexadecimal (e.g., *0x57CEF7*), +or scientific notation (e.g., *1e3* for *1000*). +A leading zero is not required (e.g., *.5* for *0.5*). + + +## Functions supported + +TinyExpr supports addition (+), subtraction/negation (-), multiplication (\*), +division (/), exponentiation (^) and modulus (%) with the normal operator +precedence (the one exception being that exponentiation is evaluated +left-to-right, but this can be changed - see below). + +The following C math functions are also supported: + +- abs (calls to *fabs*), acos, asin, atan, atan2, ceil, cos, cosh, exp, floor, ln (calls to *log*), log (calls to *log10* by default, see below), log10, pow, sin, sinh, sqrt, tan, tanh + +The following functions are also built-in and provided by TinyExpr: + +- fac (factorials e.g. `fac 5` == 120) +- ncr (combinations e.g. `ncr(6,2)` == 15) +- npr (permutations e.g. `npr(6,2)` == 30) + +Also, the following constants are available: + +- `pi`, `e` + + +## Compile-time options + + +By default, TinyExpr does exponentiation from left to right. For example: + +`a^b^c == (a^b)^c` and `-a^b == (-a)^b` + +This is by design. It's the way that spreadsheets do it (e.g. Excel, Google Sheets). + + +If you would rather have exponentiation work from right to left, you need to +define `TE_POW_FROM_RIGHT` when compiling `tinyexpr.c`. There is a +commented-out define near the top of that file. With this option enabled, the +behaviour is: + +`a^b^c == a^(b^c)` and `-a^b == -(a^b)` + +That will match how many scripting languages do it (e.g. Python, Ruby). + +Also, if you'd like `log` to default to the natural log instead of `log10`, +then you can define `TE_NAT_LOG`. + +## Hints + +- All functions/types start with the letters *te*. + +- To allow constant optimization, surround constant expressions in parentheses. + For example "x+(1+5)" will evaluate the "(1+5)" expression at compile time and + compile the entire expression as "x+6", saving a runtime calculation. The + parentheses are important, because TinyExpr will not change the order of + evaluation. If you instead compiled "x+1+5" TinyExpr will insist that "1" is + added to "x" first, and "5" is added the result second. + diff --git a/lib/tinyexpr_vendor/library.json b/lib/tinyexpr_vendor/library.json new file mode 100644 index 0000000..4b4a8de --- /dev/null +++ b/lib/tinyexpr_vendor/library.json @@ -0,0 +1,17 @@ +{ + "name": "tinyexpr_vendor", + "version": "0.0.0+4a7456e", + "description": "Vendored tinyexpr from codeplea/tinyexpr commit 4a7456e2eab88b4c76053c1c4157639ccb930e2b", + "keywords": "math, expression, parser", + "repository": { + "type": "git", + "url": "https://github.com/codeplea/tinyexpr" + }, + "license": "Zlib", + "frameworks": "*", + "platforms": "*", + "build": { + "srcDir": "src", + "includeDir": "src" + } +} diff --git a/lib/tinyexpr_vendor/src/tinyexpr.c b/lib/tinyexpr_vendor/src/tinyexpr.c new file mode 100755 index 0000000..176e8d0 --- /dev/null +++ b/lib/tinyexpr_vendor/src/tinyexpr.c @@ -0,0 +1,734 @@ +// SPDX-License-Identifier: Zlib +/* + * TINYEXPR - Tiny recursive descent parser and evaluation engine in C + * + * Copyright (c) 2015-2020 Lewis Van Winkle + * + * http://CodePlea.com + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgement in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +/* COMPILE TIME OPTIONS */ + +/* Exponentiation associativity: +For a^b^c = (a^b)^c and -a^b = (-a)^b do nothing. +For a^b^c = a^(b^c) and -a^b = -(a^b) uncomment the next line.*/ +/* #define TE_POW_FROM_RIGHT */ + +/* Logarithms +For log = base 10 log do nothing +For log = natural log uncomment the next line. */ +/* #define TE_NAT_LOG */ + +#include "tinyexpr.h" +#include +#include +#include +#include +#include +#include + +#ifndef NAN +#define NAN (0.0/0.0) +#endif + +#ifndef INFINITY +#define INFINITY (1.0/0.0) +#endif + + +typedef double (*te_fun2)(double, double); + +enum { + TOK_NULL = TE_CLOSURE7+1, TOK_ERROR, TOK_END, TOK_SEP, + TOK_OPEN, TOK_CLOSE, TOK_NUMBER, TOK_VARIABLE, TOK_INFIX +}; + + +enum {TE_CONSTANT = 1}; + + +typedef struct state { + const char *start; + const char *next; + int type; + union {double value; const double *bound; const void *function;}; + void *context; + + const te_variable *lookup; + int lookup_len; +} state; + + +#define TYPE_MASK(TYPE) ((TYPE)&0x0000001F) + +#define IS_PURE(TYPE) (((TYPE) & TE_FLAG_PURE) != 0) +#define IS_FUNCTION(TYPE) (((TYPE) & TE_FUNCTION0) != 0) +#define IS_CLOSURE(TYPE) (((TYPE) & TE_CLOSURE0) != 0) +#define ARITY(TYPE) ( ((TYPE) & (TE_FUNCTION0 | TE_CLOSURE0)) ? ((TYPE) & 0x00000007) : 0 ) +#define NEW_EXPR(type, ...) new_expr((type), (const te_expr*[]){__VA_ARGS__}) +#define CHECK_NULL(ptr, ...) if ((ptr) == NULL) { __VA_ARGS__; return NULL; } + +static te_expr *new_expr(const int type, const te_expr *parameters[]) { + const int arity = ARITY(type); + const int psize = sizeof(void*) * arity; + const int size = (sizeof(te_expr) - sizeof(void*)) + psize + (IS_CLOSURE(type) ? sizeof(void*) : 0); + te_expr *ret = malloc(size); + CHECK_NULL(ret); + + memset(ret, 0, size); + if (arity && parameters) { + memcpy(ret->parameters, parameters, psize); + } + ret->type = type; + ret->bound = 0; + return ret; +} + + +void te_free_parameters(te_expr *n) { + if (!n) return; + switch (TYPE_MASK(n->type)) { + case TE_FUNCTION7: case TE_CLOSURE7: te_free(n->parameters[6]); /* Falls through. */ + case TE_FUNCTION6: case TE_CLOSURE6: te_free(n->parameters[5]); /* Falls through. */ + case TE_FUNCTION5: case TE_CLOSURE5: te_free(n->parameters[4]); /* Falls through. */ + case TE_FUNCTION4: case TE_CLOSURE4: te_free(n->parameters[3]); /* Falls through. */ + case TE_FUNCTION3: case TE_CLOSURE3: te_free(n->parameters[2]); /* Falls through. */ + case TE_FUNCTION2: case TE_CLOSURE2: te_free(n->parameters[1]); /* Falls through. */ + case TE_FUNCTION1: case TE_CLOSURE1: te_free(n->parameters[0]); + } +} + + +void te_free(te_expr *n) { + if (!n) return; + te_free_parameters(n); + free(n); +} + + +static double pi(void) {return 3.14159265358979323846;} +static double e(void) {return 2.71828182845904523536;} +static double fac(double a) {/* simplest version of fac */ + if (a < 0.0) + return NAN; + if (a > UINT_MAX) + return INFINITY; + unsigned int ua = (unsigned int)(a); + unsigned long int result = 1, i; + for (i = 1; i <= ua; i++) { + if (i > ULONG_MAX / result) + return INFINITY; + result *= i; + } + return (double)result; +} +static double ncr(double n, double r) { + if (n < 0.0 || r < 0.0 || n < r) return NAN; + if (n > UINT_MAX || r > UINT_MAX) return INFINITY; + unsigned long int un = (unsigned int)(n), ur = (unsigned int)(r), i; + unsigned long int result = 1; + if (ur > un / 2) ur = un - ur; + for (i = 1; i <= ur; i++) { + if (result > ULONG_MAX / (un - ur + i)) + return INFINITY; + result *= un - ur + i; + result /= i; + } + return result; +} +static double npr(double n, double r) {return ncr(n, r) * fac(r);} + +#ifdef _MSC_VER +#pragma function (ceil) +#pragma function (floor) +#endif + +static const te_variable functions[] = { + /* must be in alphabetical order */ + {"abs", fabs, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"acos", acos, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"asin", asin, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"atan", atan, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"atan2", atan2, TE_FUNCTION2 | TE_FLAG_PURE, 0}, + {"ceil", ceil, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"cos", cos, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"cosh", cosh, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"e", e, TE_FUNCTION0 | TE_FLAG_PURE, 0}, + {"exp", exp, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"fac", fac, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"floor", floor, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"ln", log, TE_FUNCTION1 | TE_FLAG_PURE, 0}, +#ifdef TE_NAT_LOG + {"log", log, TE_FUNCTION1 | TE_FLAG_PURE, 0}, +#else + {"log", log10, TE_FUNCTION1 | TE_FLAG_PURE, 0}, +#endif + {"log10", log10, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"ncr", ncr, TE_FUNCTION2 | TE_FLAG_PURE, 0}, + {"npr", npr, TE_FUNCTION2 | TE_FLAG_PURE, 0}, + {"pi", pi, TE_FUNCTION0 | TE_FLAG_PURE, 0}, + {"pow", pow, TE_FUNCTION2 | TE_FLAG_PURE, 0}, + {"sin", sin, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"sinh", sinh, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"sqrt", sqrt, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"tan", tan, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"tanh", tanh, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {0, 0, 0, 0} +}; + +static const te_variable *find_builtin(const char *name, int len) { + int imin = 0; + int imax = sizeof(functions) / sizeof(te_variable) - 2; + + /*Binary search.*/ + while (imax >= imin) { + const int i = (imin + ((imax-imin)/2)); + int c = strncmp(name, functions[i].name, len); + if (!c) c = '\0' - functions[i].name[len]; + if (c == 0) { + return functions + i; + } else if (c > 0) { + imin = i + 1; + } else { + imax = i - 1; + } + } + + return 0; +} + +static const te_variable *find_lookup(const state *s, const char *name, int len) { + int iters; + const te_variable *var; + if (!s->lookup) return 0; + + for (var = s->lookup, iters = s->lookup_len; iters; ++var, --iters) { + if (strncmp(name, var->name, len) == 0 && var->name[len] == '\0') { + return var; + } + } + return 0; +} + + + +static double add(double a, double b) {return a + b;} +static double sub(double a, double b) {return a - b;} +static double mul(double a, double b) {return a * b;} +static double divide(double a, double b) {return a / b;} +static double negate(double a) {return -a;} +static double comma(double a, double b) {(void)a; return b;} + + +void next_token(state *s) { + s->type = TOK_NULL; + + do { + + if (!*s->next){ + s->type = TOK_END; + return; + } + + /* Try reading a number. */ + if ((s->next[0] >= '0' && s->next[0] <= '9') || s->next[0] == '.') { + s->value = strtod(s->next, (char**)&s->next); + s->type = TOK_NUMBER; + } else { + /* Look for a variable or builtin function call. */ + if (isalpha(s->next[0])) { + const char *start; + start = s->next; + while (isalpha(s->next[0]) || isdigit(s->next[0]) || (s->next[0] == '_')) s->next++; + + const te_variable *var = find_lookup(s, start, s->next - start); + if (!var) var = find_builtin(start, s->next - start); + + if (!var) { + s->type = TOK_ERROR; + } else { + switch(TYPE_MASK(var->type)) + { + case TE_VARIABLE: + s->type = TOK_VARIABLE; + s->bound = var->address; + break; + + case TE_CLOSURE0: case TE_CLOSURE1: case TE_CLOSURE2: case TE_CLOSURE3: /* Falls through. */ + case TE_CLOSURE4: case TE_CLOSURE5: case TE_CLOSURE6: case TE_CLOSURE7: /* Falls through. */ + s->context = var->context; /* Falls through. */ + + case TE_FUNCTION0: case TE_FUNCTION1: case TE_FUNCTION2: case TE_FUNCTION3: /* Falls through. */ + case TE_FUNCTION4: case TE_FUNCTION5: case TE_FUNCTION6: case TE_FUNCTION7: /* Falls through. */ + s->type = var->type; + s->function = var->address; + break; + } + } + + } else { + /* Look for an operator or special character. */ + switch (s->next++[0]) { + case '+': s->type = TOK_INFIX; s->function = add; break; + case '-': s->type = TOK_INFIX; s->function = sub; break; + case '*': s->type = TOK_INFIX; s->function = mul; break; + case '/': s->type = TOK_INFIX; s->function = divide; break; + case '^': s->type = TOK_INFIX; s->function = pow; break; + case '%': s->type = TOK_INFIX; s->function = fmod; break; + case '(': s->type = TOK_OPEN; break; + case ')': s->type = TOK_CLOSE; break; + case ',': s->type = TOK_SEP; break; + case ' ': case '\t': case '\n': case '\r': break; + default: s->type = TOK_ERROR; break; + } + } + } + } while (s->type == TOK_NULL); +} + + +static te_expr *list(state *s); +static te_expr *expr(state *s); +static te_expr *power(state *s); + +static te_expr *base(state *s) { + /* = | | {"(" ")"} | | "(" {"," } ")" | "(" ")" */ + te_expr *ret; + int arity; + + switch (TYPE_MASK(s->type)) { + case TOK_NUMBER: + ret = new_expr(TE_CONSTANT, 0); + CHECK_NULL(ret); + + ret->value = s->value; + next_token(s); + break; + + case TOK_VARIABLE: + ret = new_expr(TE_VARIABLE, 0); + CHECK_NULL(ret); + + ret->bound = s->bound; + next_token(s); + break; + + case TE_FUNCTION0: + case TE_CLOSURE0: + ret = new_expr(s->type, 0); + CHECK_NULL(ret); + + ret->function = s->function; + if (IS_CLOSURE(s->type)) ret->parameters[0] = s->context; + next_token(s); + if (s->type == TOK_OPEN) { + next_token(s); + if (s->type != TOK_CLOSE) { + s->type = TOK_ERROR; + } else { + next_token(s); + } + } + break; + + case TE_FUNCTION1: + case TE_CLOSURE1: + ret = new_expr(s->type, 0); + CHECK_NULL(ret); + + ret->function = s->function; + if (IS_CLOSURE(s->type)) ret->parameters[1] = s->context; + next_token(s); + ret->parameters[0] = power(s); + CHECK_NULL(ret->parameters[0], te_free(ret)); + break; + + case TE_FUNCTION2: case TE_FUNCTION3: case TE_FUNCTION4: + case TE_FUNCTION5: case TE_FUNCTION6: case TE_FUNCTION7: + case TE_CLOSURE2: case TE_CLOSURE3: case TE_CLOSURE4: + case TE_CLOSURE5: case TE_CLOSURE6: case TE_CLOSURE7: + arity = ARITY(s->type); + + ret = new_expr(s->type, 0); + CHECK_NULL(ret); + + ret->function = s->function; + if (IS_CLOSURE(s->type)) ret->parameters[arity] = s->context; + next_token(s); + + if (s->type != TOK_OPEN) { + s->type = TOK_ERROR; + } else { + int i; + for(i = 0; i < arity; i++) { + next_token(s); + ret->parameters[i] = expr(s); + CHECK_NULL(ret->parameters[i], te_free(ret)); + + if(s->type != TOK_SEP) { + break; + } + } + if(s->type != TOK_CLOSE || i != arity - 1) { + s->type = TOK_ERROR; + } else { + next_token(s); + } + } + + break; + + case TOK_OPEN: + next_token(s); + ret = list(s); + CHECK_NULL(ret); + + if (s->type != TOK_CLOSE) { + s->type = TOK_ERROR; + } else { + next_token(s); + } + break; + + default: + ret = new_expr(0, 0); + CHECK_NULL(ret); + + s->type = TOK_ERROR; + ret->value = NAN; + break; + } + + return ret; +} + + +static te_expr *power(state *s) { + /* = {("-" | "+")} */ + int sign = 1; + while (s->type == TOK_INFIX && (s->function == add || s->function == sub)) { + if (s->function == sub) sign = -sign; + next_token(s); + } + + te_expr *ret; + + if (sign == 1) { + ret = base(s); + } else { + te_expr *b = base(s); + CHECK_NULL(b); + + ret = NEW_EXPR(TE_FUNCTION1 | TE_FLAG_PURE, b); + CHECK_NULL(ret, te_free(b)); + + ret->function = negate; + } + + return ret; +} + +#ifdef TE_POW_FROM_RIGHT +static te_expr *factor(state *s) { + /* = {"^" } */ + te_expr *ret = power(s); + CHECK_NULL(ret); + + int neg = 0; + + if (ret->type == (TE_FUNCTION1 | TE_FLAG_PURE) && ret->function == negate) { + te_expr *se = ret->parameters[0]; + free(ret); + ret = se; + neg = 1; + } + + te_expr *insertion = 0; + + while (s->type == TOK_INFIX && (s->function == pow)) { + te_fun2 t = s->function; + next_token(s); + + if (insertion) { + /* Make exponentiation go right-to-left. */ + te_expr *p = power(s); + CHECK_NULL(p, te_free(ret)); + + te_expr *insert = NEW_EXPR(TE_FUNCTION2 | TE_FLAG_PURE, insertion->parameters[1], p); + CHECK_NULL(insert, te_free(p), te_free(ret)); + + insert->function = t; + insertion->parameters[1] = insert; + insertion = insert; + } else { + te_expr *p = power(s); + CHECK_NULL(p, te_free(ret)); + + te_expr *prev = ret; + ret = NEW_EXPR(TE_FUNCTION2 | TE_FLAG_PURE, ret, p); + CHECK_NULL(ret, te_free(p), te_free(prev)); + + ret->function = t; + insertion = ret; + } + } + + if (neg) { + te_expr *prev = ret; + ret = NEW_EXPR(TE_FUNCTION1 | TE_FLAG_PURE, ret); + CHECK_NULL(ret, te_free(prev)); + + ret->function = negate; + } + + return ret; +} +#else +static te_expr *factor(state *s) { + /* = {"^" } */ + te_expr *ret = power(s); + CHECK_NULL(ret); + + while (s->type == TOK_INFIX && (s->function == pow)) { + te_fun2 t = s->function; + next_token(s); + te_expr *p = power(s); + CHECK_NULL(p, te_free(ret)); + + te_expr *prev = ret; + ret = NEW_EXPR(TE_FUNCTION2 | TE_FLAG_PURE, ret, p); + CHECK_NULL(ret, te_free(p), te_free(prev)); + + ret->function = t; + } + + return ret; +} +#endif + + + +static te_expr *term(state *s) { + /* = {("*" | "/" | "%") } */ + te_expr *ret = factor(s); + CHECK_NULL(ret); + + while (s->type == TOK_INFIX && (s->function == mul || s->function == divide || s->function == fmod)) { + te_fun2 t = s->function; + next_token(s); + te_expr *f = factor(s); + CHECK_NULL(f, te_free(ret)); + + te_expr *prev = ret; + ret = NEW_EXPR(TE_FUNCTION2 | TE_FLAG_PURE, ret, f); + CHECK_NULL(ret, te_free(f), te_free(prev)); + + ret->function = t; + } + + return ret; +} + + +static te_expr *expr(state *s) { + /* = {("+" | "-") } */ + te_expr *ret = term(s); + CHECK_NULL(ret); + + while (s->type == TOK_INFIX && (s->function == add || s->function == sub)) { + te_fun2 t = s->function; + next_token(s); + te_expr *te = term(s); + CHECK_NULL(te, te_free(ret)); + + te_expr *prev = ret; + ret = NEW_EXPR(TE_FUNCTION2 | TE_FLAG_PURE, ret, te); + CHECK_NULL(ret, te_free(te), te_free(prev)); + + ret->function = t; + } + + return ret; +} + + +static te_expr *list(state *s) { + /* = {"," } */ + te_expr *ret = expr(s); + CHECK_NULL(ret); + + while (s->type == TOK_SEP) { + next_token(s); + te_expr *e = expr(s); + CHECK_NULL(e, te_free(ret)); + + te_expr *prev = ret; + ret = NEW_EXPR(TE_FUNCTION2 | TE_FLAG_PURE, ret, e); + CHECK_NULL(ret, te_free(e), te_free(prev)); + + ret->function = comma; + } + + return ret; +} + + +#define TE_FUN(...) ((double(*)(__VA_ARGS__))n->function) +#define M(e) te_eval(n->parameters[e]) + + +double te_eval(const te_expr *n) { + if (!n) return NAN; + + switch(TYPE_MASK(n->type)) { + case TE_CONSTANT: return n->value; + case TE_VARIABLE: return *n->bound; + + case TE_FUNCTION0: case TE_FUNCTION1: case TE_FUNCTION2: case TE_FUNCTION3: + case TE_FUNCTION4: case TE_FUNCTION5: case TE_FUNCTION6: case TE_FUNCTION7: + switch(ARITY(n->type)) { + case 0: return TE_FUN(void)(); + case 1: return TE_FUN(double)(M(0)); + case 2: return TE_FUN(double, double)(M(0), M(1)); + case 3: return TE_FUN(double, double, double)(M(0), M(1), M(2)); + case 4: return TE_FUN(double, double, double, double)(M(0), M(1), M(2), M(3)); + case 5: return TE_FUN(double, double, double, double, double)(M(0), M(1), M(2), M(3), M(4)); + case 6: return TE_FUN(double, double, double, double, double, double)(M(0), M(1), M(2), M(3), M(4), M(5)); + case 7: return TE_FUN(double, double, double, double, double, double, double)(M(0), M(1), M(2), M(3), M(4), M(5), M(6)); + default: return NAN; + } + + case TE_CLOSURE0: case TE_CLOSURE1: case TE_CLOSURE2: case TE_CLOSURE3: + case TE_CLOSURE4: case TE_CLOSURE5: case TE_CLOSURE6: case TE_CLOSURE7: + switch(ARITY(n->type)) { + case 0: return TE_FUN(void*)(n->parameters[0]); + case 1: return TE_FUN(void*, double)(n->parameters[1], M(0)); + case 2: return TE_FUN(void*, double, double)(n->parameters[2], M(0), M(1)); + case 3: return TE_FUN(void*, double, double, double)(n->parameters[3], M(0), M(1), M(2)); + case 4: return TE_FUN(void*, double, double, double, double)(n->parameters[4], M(0), M(1), M(2), M(3)); + case 5: return TE_FUN(void*, double, double, double, double, double)(n->parameters[5], M(0), M(1), M(2), M(3), M(4)); + case 6: return TE_FUN(void*, double, double, double, double, double, double)(n->parameters[6], M(0), M(1), M(2), M(3), M(4), M(5)); + case 7: return TE_FUN(void*, double, double, double, double, double, double, double)(n->parameters[7], M(0), M(1), M(2), M(3), M(4), M(5), M(6)); + default: return NAN; + } + + default: return NAN; + } + +} + +#undef TE_FUN +#undef M + +static void optimize(te_expr *n) { + /* Evaluates as much as possible. */ + if (n->type == TE_CONSTANT) return; + if (n->type == TE_VARIABLE) return; + + /* Only optimize out functions flagged as pure. */ + if (IS_PURE(n->type)) { + const int arity = ARITY(n->type); + int known = 1; + int i; + for (i = 0; i < arity; ++i) { + optimize(n->parameters[i]); + if (((te_expr*)(n->parameters[i]))->type != TE_CONSTANT) { + known = 0; + } + } + if (known) { + const double value = te_eval(n); + te_free_parameters(n); + n->type = TE_CONSTANT; + n->value = value; + } + } +} + + +te_expr *te_compile(const char *expression, const te_variable *variables, int var_count, int *error) { + state s; + s.start = s.next = expression; + s.lookup = variables; + s.lookup_len = var_count; + + next_token(&s); + te_expr *root = list(&s); + if (root == NULL) { + if (error) *error = -1; + return NULL; + } + + if (s.type != TOK_END) { + te_free(root); + if (error) { + *error = (s.next - s.start); + if (*error == 0) *error = 1; + } + return 0; + } else { + optimize(root); + if (error) *error = 0; + return root; + } +} + + +double te_interp(const char *expression, int *error) { + te_expr *n = te_compile(expression, 0, 0, error); + + double ret; + if (n) { + ret = te_eval(n); + te_free(n); + } else { + ret = NAN; + } + return ret; +} + +static void pn (const te_expr *n, int depth) { + int i, arity; + printf("%*s", depth, ""); + + switch(TYPE_MASK(n->type)) { + case TE_CONSTANT: printf("%f\n", n->value); break; + case TE_VARIABLE: printf("bound %p\n", n->bound); break; + + case TE_FUNCTION0: case TE_FUNCTION1: case TE_FUNCTION2: case TE_FUNCTION3: + case TE_FUNCTION4: case TE_FUNCTION5: case TE_FUNCTION6: case TE_FUNCTION7: + case TE_CLOSURE0: case TE_CLOSURE1: case TE_CLOSURE2: case TE_CLOSURE3: + case TE_CLOSURE4: case TE_CLOSURE5: case TE_CLOSURE6: case TE_CLOSURE7: + arity = ARITY(n->type); + printf("f%d", arity); + for(i = 0; i < arity; i++) { + printf(" %p", n->parameters[i]); + } + printf("\n"); + for(i = 0; i < arity; i++) { + pn(n->parameters[i], depth + 1); + } + break; + } +} + + +void te_print(const te_expr *n) { + pn(n, 0); +} diff --git a/lib/tinyexpr_vendor/src/tinyexpr.h b/lib/tinyexpr_vendor/src/tinyexpr.h new file mode 100644 index 0000000..c2cbe1a --- /dev/null +++ b/lib/tinyexpr_vendor/src/tinyexpr.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Zlib +/* + * TINYEXPR - Tiny recursive descent parser and evaluation engine in C + * + * Copyright (c) 2015-2020 Lewis Van Winkle + * + * http://CodePlea.com + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgement in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#ifndef TINYEXPR_H +#define TINYEXPR_H + + +#ifdef __cplusplus +extern "C" { +#endif + + + +typedef struct te_expr { + int type; + union {double value; const double *bound; const void *function;}; + void *parameters[1]; +} te_expr; + + +enum { + TE_VARIABLE = 0, + + TE_FUNCTION0 = 8, TE_FUNCTION1, TE_FUNCTION2, TE_FUNCTION3, + TE_FUNCTION4, TE_FUNCTION5, TE_FUNCTION6, TE_FUNCTION7, + + TE_CLOSURE0 = 16, TE_CLOSURE1, TE_CLOSURE2, TE_CLOSURE3, + TE_CLOSURE4, TE_CLOSURE5, TE_CLOSURE6, TE_CLOSURE7, + + TE_FLAG_PURE = 32 +}; + +typedef struct te_variable { + const char *name; + const void *address; + int type; + void *context; +} te_variable; + + + +/* Parses the input expression, evaluates it, and frees it. */ +/* Returns NaN on error. */ +double te_interp(const char *expression, int *error); + +/* Parses the input expression and binds variables. */ +/* Returns NULL on error. */ +te_expr *te_compile(const char *expression, const te_variable *variables, int var_count, int *error); + +/* Evaluates the expression. */ +double te_eval(const te_expr *n); + +/* Prints debugging information on the syntax tree. */ +void te_print(const te_expr *n); + +/* Frees the expression. */ +/* This is safe to call on NULL pointers. */ +void te_free(te_expr *n); + + +#ifdef __cplusplus +} +#endif + +#endif /*TINYEXPR_H*/ diff --git a/partitions/freenove_esp32s3_app4mb_fs12096kb.csv b/partitions/freenove_esp32s3_app4mb_fs12096kb.csv new file mode 100644 index 0000000..e5f1bc4 --- /dev/null +++ b/partitions/freenove_esp32s3_app4mb_fs12096kb.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x6000, +otadata, data, ota, 0xF000, 0x2000, +app0, app, ota_0, 0x20000, 0x400000, +spiffs, data, spiffs, 0x420000, 0xBD0000, +coredump, data, coredump,0xFF0000, 0x10000, diff --git a/partitions/freenove_esp32s3_app4mb_fs3840kb.csv b/partitions/freenove_esp32s3_app4mb_fs3840kb.csv new file mode 100644 index 0000000..810d7e0 --- /dev/null +++ b/partitions/freenove_esp32s3_app4mb_fs3840kb.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x6000, +otadata, data, ota, 0xF000, 0x2000, +app0, app, ota_0, 0x20000, 0x400000, +spiffs, data, spiffs, 0x420000, 0x3C0000, +coredump, data, coredump,0x7E0000, 0x10000, diff --git a/platformio.ini b/platformio.ini index 44d43aa..99e87af 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,85 +1,28 @@ # -# PlatformIO Project Configuration File (global) -# Permet de builder tous les firmwares depuis la racine du projet +# PlatformIO Project Configuration File +# Cible release : Freenove FNK0102H / Freenove ESP32-S3 WROOM N16R8 # [platformio] default_envs = freenove_esp32s3_full_with_ui src_dir = ui_freenove_allinone -; ===================== ESP32 ===================== - -[env:esp32dev] -platform = espressif32 -board = esp32dev -framework = arduino -board_build.partitions = no_ota.csv -board_build.filesystem = littlefs -build_src_filter = - -<*> - + - +<../libs/story/src/> - +<../libs/story/story_engine.cpp> - -<../libs/story/src/ui/mp3_ui_model_v2.cpp> -monitor_speed = 115200 -monitor_filters = time, esp32_exception_decoder -monitor_echo = yes -monitor_eol = LF -monitor_rts = 0 -monitor_dtr = 0 -lib_deps = - earlephilhower/ESP8266Audio@^1.9.7 - https://github.com/pschatzmann/arduino-audio-tools.git#v1.2.2 - sensorium/Mozzi@^2.0.2 - https://github.com/pschatzmann/arduino-audio-driver.git#84c7de2eb47efe43bcc769783bb1853ee8a32f21 - bblanchon/ArduinoJson@^6.21.5 - https://github.com/me-no-dev/AsyncTCP.git - https://github.com/me-no-dev/ESPAsyncWebServer.git -build_flags = - -I$PROJECT_DIR/protocol - -Ihardware/firmware/esp32_audio/src - -Ihardware/libs/story/src - -Ihardware/libs/story - -DCORE_DEBUG_LEVEL=1 - -DUSON_STORY_V2_DEFAULT=1 - -DUSON_TRACK_CATALOG_MAX_TRACKS=200 - ; ===================== freenove_esp32s3_full_with_ui ===================== [env:freenove_esp32s3_full_with_ui] -platform = espressif32@^6.5.0 -# Physical board: ESP32-S3-WROOM-1-N16R8 (16MB flash / 16MB PSRAM) -board = esp32-s3-devkitc-1 +platform = espressif32@6.5.0 +# Physical board profile: Freenove ESP32-S3 WROOM N16R8 (16MB flash / 8MB PSRAM) +board = freenove_esp32_s3_wroom framework = arduino -board_build.arduino.memory_type = qio_opi -board_build.psram_type = opi -upload_port = /dev/cu.usbmodem5AB907* -monitor_port = /dev/cu.usbmodem5AB907* extra_scripts = pre:scripts/pio_prepare_webui_fonts.py board_build.flash_size = 16MB board_upload.flash_size = 16MB -board_upload.maximum_size = 6291456 +board_upload.maximum_size = 4194304 board_upload.offset_address = 0x20000 -board_build.partitions = partitions/freenove_esp32s3_app6mb_fs6mb.csv +board_build.partitions = partitions/freenove_esp32s3_app4mb_fs12096kb.csv board_build.filesystem = littlefs build_src_filter = -<*> + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - monitor_speed = 115200 monitor_filters = time, esp32_exception_decoder monitor_echo = yes @@ -87,23 +30,20 @@ monitor_eol = LF monitor_rts = 0 monitor_dtr = 0 lib_deps = - bodmer/TFT_eSPI@^2.5.43 - lovyan03/LovyanGFX@^1.2.7 - lvgl/lvgl@^8.4.0 - esphome/ESP32-audioI2S@^2.3.0 - bblanchon/ArduinoJson@^6.21.5 - adafruit/Adafruit NeoPixel@^1.12.3 - https://github.com/alvarowolfx/ESP32QRCodeReader.git#dd07abcf062c4c1b120e0584df57c0c92ca77daf - https://github.com/codeplea/tinyexpr.git#4a7456e2eab88b4c76053c1c4157639ccb930e2b + bodmer/TFT_eSPI@2.5.43 + lovyan03/LovyanGFX@1.2.7 + lvgl/lvgl@8.4.0 + esphome/ESP32-audioI2S@2.3.0 + bblanchon/ArduinoJson@6.21.5 + adafruit/Adafruit NeoPixel@1.12.3 build_flags = - -I$PROJECT_DIR/protocol -I$PROJECT_DIR/ui_freenove_allinone/include -I$PROJECT_DIR/lib/zacus_story_portable/include -O2 -ffast-math -DCORE_DEBUG_LEVEL=0 -DARDUINO_LOOP_STACK_SIZE=16384 - -DZACUS_SPRINT_DIAG_MODE=1 + -DZACUS_SPRINT_DIAG_MODE=0 -DBOARD_HAS_PSRAM -DFREENOVE_USE_PSRAM=1 -DFREENOVE_PSRAM_UI_DRAW_BUFFER=1 @@ -157,138 +97,17 @@ extends = env:freenove_esp32s3_full_with_ui [env:freenove_esp32s3_touch] extends = env:freenove_esp32s3_full_with_ui +# Experimental only: release builds keep touch disabled because the official +# FNK0102H profile is the 5-way switch, and GPIO9/GPIO15 are already used by camera wiring. build_unflags = -DFREENOVE_HAS_TOUCH=0 build_flags = ${env:freenove_esp32s3_full_with_ui.build_flags} -DFREENOVE_HAS_TOUCH=1 -[env:esp32_release] -extends = env:esp32dev -build_unflags = -DUSON_STORY_V2_DEFAULT=1 -build_flags = ${env:esp32dev.build_flags} -DUSON_STORY_V2_DEFAULT=0 - -; ===================== ui_rp2040_ili9488 ===================== - -[env:ui_rp2040_ili9488] -platform = https://github.com/maxgerhardt/platform-raspberrypi.git -board = rpipico -framework = arduino -build_src_filter = - -<*> - + -monitor_speed = 115200 -board_build.filesystem = littlefs -lib_deps = - bodmer/TFT_eSPI@^2.5.43 - paulstoffregen/XPT2046_Touchscreen@0.0.0-alpha+sha.26b691b2c8 - bblanchon/ArduinoJson@^6.21.5 - lvgl/lvgl@^8.3.11 -build_flags = - -I$PROJECT_DIR/protocol - -Ihardware/firmware/ui/rp2040_tft/include - -DLV_CONF_INCLUDE_SIMPLE - -DUSER_SETUP_LOADED=1 - -DTFT_MISO=4 - -DTFT_MOSI=3 - -DTFT_SCLK=2 - -DTFT_CS=5 - -DTFT_DC=6 - -DTFT_RST=7 - -DTOUCH_CS=9 - -DSPI_FREQUENCY=40000000 - -DSPI_READ_FREQUENCY=20000000 - -DSPI_TOUCH_FREQUENCY=2500000 - -DSUPPORT_TRANSACTIONS - -DUI_LCD_SPI_HOST=0 - -DUI_TOUCH_IRQ_PIN=15 - -DUI_UART_RX_PIN=1 - -DUI_UART_TX_PIN=0 - -DUI_SERIAL_BAUD=57600 - -DUI_ROTATION=1 - -DILI9488_DRIVER - -DTFT_RGB_ORDER=TFT_BGR - - - -; ===================== (ui_rp2040_ili9486) ===================== - -[env:ui_rp2040_ili9486] -platform = https://github.com/maxgerhardt/platform-raspberrypi.git -board = rpipico -framework = arduino -build_src_filter = - -<*> - + -monitor_speed = 115200 -board_build.filesystem = littlefs -lib_deps = - bodmer/TFT_eSPI@^2.5.43 - paulstoffregen/XPT2046_Touchscreen@0.0.0-alpha+sha.26b691b2c8 - bblanchon/ArduinoJson@^6.21.5 - lvgl/lvgl@^8.3.11 -build_flags = - -I$PROJECT_DIR/protocol - -Ihardware/firmware/ui/rp2040_tft/include - -DLV_CONF_INCLUDE_SIMPLE - -DUSER_SETUP_LOADED=1 - -DFREENOVE_MEDIA_KIT - -include ui_freenove_config.h - -DFREENOVE_LCD_WIDTH=480 - -DFREENOVE_LCD_HEIGHT=320 - -DFREENOVE_TFT_CS=5 - -DFREENOVE_TFT_DC=6 - -DFREENOVE_TFT_RST=7 - -DFREENOVE_TFT_MOSI=3 - -DFREENOVE_TFT_MISO=4 - -DFREENOVE_TFT_SCK=2 - -DFREENOVE_TOUCH_CS=9 - -DFREENOVE_TOUCH_IRQ=15 - -DFREENOVE_UART_TX=0 - -DFREENOVE_UART_RX=1 - -DFREENOVE_LCD_ROTATION=1 - -DSPI_FREQUENCY=40000000 - -DSPI_READ_FREQUENCY=20000000 - -DSPI_TOUCH_FREQUENCY=2500000 - -DSUPPORT_TRANSACTIONS - -DUI_LCD_SPI_HOST=0 - -DUI_SERIAL_BAUD=57600 - -DUI_ROTATION=1 - -DILI9488_DRIVER - -DTFT_RGB_ORDER=TFT_BGR - -; ===================== ESP8266 OLED ===================== - -[env:esp8266_oled] -platform = espressif8266 -board = nodemcuv2 -framework = arduino -build_src_filter = - -<*> - + -monitor_speed = 115200 -monitor_filters = time, colorize, esp8266_exception_decoder -monitor_echo = yes -monitor_eol = LF -monitor_rts = 0 -monitor_dtr = 0 -lib_deps = - adafruit/Adafruit SSD1306@^2.5.13 - adafruit/Adafruit GFX Library@^1.12.1 - plerup/EspSoftwareSerial@^8.2.0 - olikraus/U8g2@^2.36.2 - bblanchon/ArduinoJson@^6.21.5 -build_flags = - -Iprotocol - # -# Pour builder tous les firmwares : -# pio run -# Pour builder un firmware spécifique : -# pio run -e -# Pour flasher/monitorer : -# pio run -e -t upload --upload-port -# pio device monitor -e --port -# -# Voir docs/protocols/ pour la documentation centralisée +# Builder : pio run +# Flasher : pio run -t upload +# Monitor : pio device monitor +# Filesystem : pio run -t uploadfs # diff --git a/scripts/audit_boot_log.py b/scripts/audit_boot_log.py new file mode 100755 index 0000000..2be99e6 --- /dev/null +++ b/scripts/audit_boot_log.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + + +EXIT_CODES = { + "normal_boot_ok": 0, + "safe_diagnostic_ok": 10, + "build_blocked": 20, + "runtime_regression": 30, +} + + +def load_text(path: Path | None) -> str: + if path is None or not path.exists(): + return "" + return path.read_text(encoding="utf-8", errors="ignore") + + +def relevant_segment(text: str) -> str: + for marker in ("[HOST] ", "[MAIN] Freenove all-in-one boot", "[BOOT] safe diagnostic mode enabled"): + idx = text.rfind(marker if marker != "[HOST] " else "SENT RESET") + if idx != -1: + return text[idx:] + return text + + +def has_any(text: str, needles: list[str]) -> bool: + return any(needle in text for needle in needles) + + +def classify(text: str, build_ok: bool, upload_ok: bool) -> tuple[str, list[str]]: + evidence: list[str] = [] + if not build_ok or not upload_ok: + if not build_ok: + evidence.append("build_ok=0") + if not upload_ok: + evidence.append("upload_ok=0") + return "build_blocked", evidence + + segment = relevant_segment(text) + psram_match = re.findall(r"psram_found=(\d)", segment) + if psram_match: + evidence.append(f"psram_found={psram_match[-1]}") + + safe_markers = [ + "[BOOT] safe diagnostic mode enabled: PSRAM required, app stack disabled", + "[SAFE] boot path: storage + serial + display + buttons only", + ] + safe_forbidden = [ + "[NET]", + "[WEB]", + "[CAM] boot start", + "wifi:alloc pp wdev funcs fail", + "tag=fx_", + "tag=fx_sprite", + ] + fatal_markers = [ + "Guru Meditation", + "Backtrace:", + "abort()", + "panic", + "wifi:alloc pp wdev funcs fail", + "[MEM] alloc_fail", + ] + + if has_any(segment, safe_markers): + evidence.extend(marker for marker in safe_markers if marker in segment) + forbidden = [marker for marker in safe_forbidden if marker in segment] + if not forbidden and "STATUS mode=safe_diagnostic" in segment: + evidence.append("STATUS mode=safe_diagnostic") + return "safe_diagnostic_ok", evidence + evidence.extend(f"forbidden={marker}" for marker in forbidden) + return "runtime_regression", evidence + + normal_markers = [ + "psram_found=1", + "LVGL + display ready", + "PONG", + ] + if has_any(segment, normal_markers): + evidence.extend(marker for marker in normal_markers if marker in segment) + fatals = [marker for marker in fatal_markers if marker in segment] + if "psram_found=1" in segment and "LVGL + display ready" in segment and not fatals: + return "normal_boot_ok", evidence + evidence.extend(f"fatal={marker}" for marker in fatals) + return "runtime_regression", evidence + + if segment.strip(): + evidence.append("boot segment captured but acceptance markers missing") + else: + evidence.append("boot log empty") + return "runtime_regression", evidence + + +def write_summary(path: Path, state: str, evidence: list[str], log_path: Path | None) -> None: + lines = [ + f"state={state}", + ] + if log_path is not None: + lines.append(f"log={log_path}") + for item in evidence: + lines.append(f"evidence={item}") + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def main() -> int: + parser = argparse.ArgumentParser(description="Classify ESP32 boot logs.") + parser.add_argument("--log") + parser.add_argument("--summary", required=True) + parser.add_argument("--build-ok", type=int, choices=(0, 1), default=1) + parser.add_argument("--upload-ok", type=int, choices=(0, 1), default=1) + args = parser.parse_args() + + log_path = Path(args.log) if args.log else None + summary_path = Path(args.summary) + text = load_text(log_path) + state, evidence = classify(text, build_ok=bool(args.build_ok), upload_ok=bool(args.upload_ok)) + write_summary(summary_path, state, evidence, log_path) + print(state) + return EXIT_CODES[state] + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/bootstrap_platformio.sh b/scripts/bootstrap_platformio.sh new file mode 100755 index 0000000..2295f4d --- /dev/null +++ b/scripts/bootstrap_platformio.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PYTHON_BIN="${PYTHON_BIN:-python3}" +VENV_DIR="${ZACUS_VENV_DIR:-$ROOT_DIR/.venv}" + +if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then + echo "[BOOTSTRAP] python3 not found" >&2 + exit 1 +fi + +if [ ! -d "$VENV_DIR" ]; then + echo "[BOOTSTRAP] creating virtualenv at $VENV_DIR" + "$PYTHON_BIN" -m venv "$VENV_DIR" +fi + +# shellcheck disable=SC1091 +source "$VENV_DIR/bin/activate" + +python -m pip install --upgrade pip +python -m pip install platformio esptool pyserial + +echo "[BOOTSTRAP] tool versions" +python -m platformio --version +python -m esptool version +python - <<'PY' +import serial +print(f"pyserial {serial.VERSION}") +PY + +cat < +python -m platformio run -e freenove_esp32s3_full_with_ui -t upload --upload-port +python -m platformio device monitor -b 115200 --port +EOF diff --git a/scripts/doctor_repo.sh b/scripts/doctor_repo.sh new file mode 100755 index 0000000..c3420fd --- /dev/null +++ b/scripts/doctor_repo.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PYTHON_BIN="${PYTHON_BIN:-python3}" +STATUS=0 + +ok() { + printf '[OK] %s\n' "$1" +} + +warn() { + printf '[WARN] %s\n' "$1" +} + +err() { + printf '[ERR] %s\n' "$1" >&2 + STATUS=1 +} + +cd "$ROOT_DIR" + +for cmd in bash rg "$PYTHON_BIN"; do + if command -v "$cmd" >/dev/null 2>&1; then + ok "tool available: $cmd" + else + err "missing tool: $cmd" + fi +done + +if command -v pio >/dev/null 2>&1; then + ok "pio available: $(pio --version | head -n 1)" +elif [ -x "$ROOT_DIR/.venv/bin/python" ] && "$ROOT_DIR/.venv/bin/python" -m platformio --version >/dev/null 2>&1; then + ok "platformio available in .venv: $("$ROOT_DIR/.venv/bin/python" -m platformio --version | head -n 1)" +else + warn "PlatformIO not found in PATH or .venv" +fi + +platform_pin="$(rg -n '^platform = ' platformio.ini | sed 's/^[0-9]*://')" +ok "platform pin: ${platform_pin:-missing}" + +echo "[INFO] supported envs" +rg '^\[env:' platformio.ini | sed 's/^\[env://; s/\]$//' + +for path in \ + README.md \ + README_ESP32_ZACUS.md \ + ui_freenove_allinone/README.md \ + docs/QUICKSTART.md \ + docs/FNK0102H_SOURCE_OF_TRUTH.md \ + boards/freenove_esp32_s3_wroom.json \ + scripts/bootstrap_platformio.sh \ + scripts/doctor_repo.sh \ + scripts/flash_monitor_audit.sh \ + scripts/serial_boot_capture.py \ + scripts/audit_boot_log.py \ + tests/sprint1_utility_contract.py \ + tests/sprint2_capture_contract.py \ + tests/sprint3_audio_contract.py \ + tests/phase9_ui_validation.py; do + if [ -e "$path" ]; then + ok "path present: $path" + else + err "path missing: $path" + fi +done + +"$PYTHON_BIN" - <<'PY' || STATUS=1 +from pathlib import Path +import re +import sys + +root = Path.cwd() +files = [ + root / "README.md", + root / "README_ESP32_ZACUS.md", + root / "ui_freenove_allinone" / "README.md", + root / "docs" / "QUICKSTART.md", + root / "docs" / "FNK0102H_SOURCE_OF_TRUTH.md", +] +legacy_needles = ("hardware/firmware", "tools/dev/", "protocol/", "../hardware/") +status = 0 + +for path in files: + text = path.read_text(encoding="utf-8") + for match in re.finditer(r"\[[^\]]+\]\(([^)]+)\)", text): + target = match.group(1).strip() + if not target or target.startswith(("http://", "https://", "#", "mailto:")): + continue + target = target.split("#", 1)[0] + if not (path.parent / target).resolve().exists(): + print(f"[ERR] broken markdown link in {path.relative_to(root)} -> {target}") + status = 1 + for needle in legacy_needles: + if needle in text: + print(f"[ERR] stale legacy reference in {path.relative_to(root)} -> {needle}") + status = 1 + +if status == 0: + print("[OK] markdown links and legacy path scan clean") +sys.exit(status) +PY + +if [ "$STATUS" -ne 0 ]; then + err "doctor detected blocking issues" +else + ok "doctor completed without blocking issues" +fi + +exit "$STATUS" diff --git a/scripts/flash_monitor_audit.sh b/scripts/flash_monitor_audit.sh new file mode 100755 index 0000000..00aa42d --- /dev/null +++ b/scripts/flash_monitor_audit.sh @@ -0,0 +1,181 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ENV_NAME="${ZACUS_ENV:-freenove_esp32s3_full_with_ui}" +MONITOR_SECONDS="${ZACUS_MONITOR_SECONDS:-90}" +DEFAULT_PORT="/dev/cu.usbmodem5AB90753301" + +if [ -x "$ROOT_DIR/.venv/bin/python" ]; then + PYTHON_BIN="$ROOT_DIR/.venv/bin/python" +else + PYTHON_BIN="${PYTHON_BIN:-python3}" +fi + +TIMESTAMP="$(date +%Y%m%d_%H%M%S)" +BUILD_ARTIFACT_DIR="$ROOT_DIR/artifacts/build" +MONITOR_ARTIFACT_DIR="$ROOT_DIR/artifacts/monitor" +AUDIT_ARTIFACT_DIR="$ROOT_DIR/artifacts/audit" +TEST_ARTIFACT_DIR="$ROOT_DIR/artifacts/tests" +SUMMARY_FILE="$AUDIT_ARTIFACT_DIR/${TIMESTAMP}_summary.txt" +MONITOR_LOG="$MONITOR_ARTIFACT_DIR/${TIMESTAMP}_boot.log" +ESPTOOL_LOG="$AUDIT_ARTIFACT_DIR/${TIMESTAMP}_esptool.log" + +mkdir -p "$BUILD_ARTIFACT_DIR" "$MONITOR_ARTIFACT_DIR" "$AUDIT_ARTIFACT_DIR" "$TEST_ARTIFACT_DIR" + +log() { + printf '[CHAIN] %s\n' "$1" +} + +detect_port() { + if [ -n "${ZACUS_SERIAL_PORT:-}" ] && [ -e "${ZACUS_SERIAL_PORT}" ]; then + printf '%s\n' "${ZACUS_SERIAL_PORT}" + return 0 + fi + if [ -e "$DEFAULT_PORT" ]; then + printf '%s\n' "$DEFAULT_PORT" + return 0 + fi + mapfile -t ports < <(find /dev -maxdepth 1 -name 'cu.usbmodem*' | sort) + if [ "${#ports[@]}" -eq 1 ]; then + printf '%s\n' "${ports[0]}" + return 0 + fi + return 1 +} + +run_stage() { + local stage="$1" + shift + local logfile="$BUILD_ARTIFACT_DIR/${TIMESTAMP}_${stage}.log" + log "stage=$stage" + if "$@" 2>&1 | tee "$logfile"; then + return 0 + fi + return 1 +} + +write_blocked_summary() { + local build_ok="$1" + local upload_ok="$2" + "$PYTHON_BIN" "$ROOT_DIR/scripts/audit_boot_log.py" \ + --summary "$SUMMARY_FILE" \ + --build-ok "$build_ok" \ + --upload-ok "$upload_ok" >/dev/null +} + +run_upload_target() { + local target="$1" + local port="$2" + run_stage "$target" "$PYTHON_BIN" -m platformio run -e "$ENV_NAME" -t "$target" --upload-port "$port" +} + +run_upload_with_retry() { + local result_var="$1" + local target="$2" + local port="$3" + if run_upload_target "$target" "$port"; then + printf -v "$result_var" '%s' "$port" + return 0 + fi + log "retry stage=$target port=$port" + if [ -e "$port" ] && run_upload_target "$target" "$port"; then + printf -v "$result_var" '%s' "$port" + return 0 + fi + local fallback_port + fallback_port="$(detect_port)" || return 1 + if [ "$fallback_port" != "$port" ] && run_upload_target "$target" "$fallback_port"; then + printf -v "$result_var" '%s' "$fallback_port" + return 0 + fi + return 1 +} + +run_test() { + local name="$1" + shift + local logfile="$TEST_ARTIFACT_DIR/${TIMESTAMP}_${name}.log" + log "test=$name" + "$@" 2>&1 | tee "$logfile" +} + +main() { + cd "$ROOT_DIR" + local port + port="$(detect_port)" || { + printf 'state=hardware_unreachable\nreason=no_serial_port\n' >"$SUMMARY_FILE" + log "no serial port detected" + return 40 + } + + export ZACUS_SERIAL_PORT="$port" + + if ! run_stage build "$PYTHON_BIN" -m platformio run -e "$ENV_NAME"; then + write_blocked_summary 0 0 + return 20 + fi + if ! run_stage buildfs "$PYTHON_BIN" -m platformio run -e "$ENV_NAME" -t buildfs; then + write_blocked_summary 0 0 + return 20 + fi + + if ! run_upload_with_retry port uploadfs "$port"; then + write_blocked_summary 1 0 + return 20 + fi + export ZACUS_SERIAL_PORT="$port" + + if ! run_upload_with_retry port upload "$port"; then + write_blocked_summary 1 0 + return 20 + fi + export ZACUS_SERIAL_PORT="$port" + + if ! "$PYTHON_BIN" "$ROOT_DIR/scripts/serial_boot_capture.py" \ + --port "$port" \ + --baud 115200 \ + --seconds "$MONITOR_SECONDS" \ + --log "$MONITOR_LOG"; then + printf 'state=hardware_unreachable\nreason=serial_capture_failed\nport=%s\nlog=%s\n' "$port" "$MONITOR_LOG" >"$SUMMARY_FILE" + return 40 + fi + + local audit_state + set +e + audit_state="$("$PYTHON_BIN" "$ROOT_DIR/scripts/audit_boot_log.py" \ + --log "$MONITOR_LOG" \ + --summary "$SUMMARY_FILE" \ + --build-ok 1 \ + --upload-ok 1)" + local audit_rc=$? + set -e + + if [ "$audit_state" = "safe_diagnostic_ok" ]; then + { + printf '[ESPTOOL] flash_id\n' + "$PYTHON_BIN" -m esptool --chip esp32s3 --port "$port" flash_id + printf '\n[ESPTOOL] read_mac\n' + "$PYTHON_BIN" -m esptool --chip esp32s3 --port "$port" read_mac + } 2>&1 | tee "$ESPTOOL_LOG" + { + printf 'finding=containment_logic_ok_root_cause_probable_hardware_or_memory_config\n' + printf 'expected_board=FNK0102H/ESP32-S3-WROOM-1-N16R8\n' + printf 'esptool_log=%s\n' "$ESPTOOL_LOG" + } >>"$SUMMARY_FILE" + return "$audit_rc" + fi + + if [ "$audit_state" = "normal_boot_ok" ]; then + run_test sprint1 "$PYTHON_BIN" "$ROOT_DIR/tests/sprint1_utility_contract.py" --mode serial --cycles 1 --port "$port" + run_test sprint2 "$PYTHON_BIN" "$ROOT_DIR/tests/sprint2_capture_contract.py" --mode serial --cycles 1 --port "$port" + run_test sprint3 "$PYTHON_BIN" "$ROOT_DIR/tests/sprint3_audio_contract.py" --mode serial --cycles 1 --port "$port" + run_test phase9 "$PYTHON_BIN" "$ROOT_DIR/tests/phase9_ui_validation.py" --port "$port" + printf 'tests=serial_smoke_pass\n' >>"$SUMMARY_FILE" + return 0 + fi + + return "$audit_rc" +} + +main "$@" diff --git a/scripts/serial_boot_capture.py b/scripts/serial_boot_capture.py new file mode 100755 index 0000000..723019c --- /dev/null +++ b/scripts/serial_boot_capture.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import sys +import time +from pathlib import Path + +import serial + + +BOOT_BANNER = "[MAIN] Freenove all-in-one boot" + + +def host_line(text: str) -> str: + stamp = time.strftime("%Y-%m-%d %H:%M:%S") + return f"[HOST] {stamp} {text}" + + +def open_serial(port: str, baud: int, timeout: float) -> serial.Serial: + ser = serial.Serial() + ser.port = port + ser.baudrate = baud + ser.timeout = timeout + ser.write_timeout = 1.0 + ser.dtr = False + ser.rts = False + ser.open() + time.sleep(0.25) + return ser + + +def flush_lines(ser: serial.Serial, log_handle, deadline: float) -> list[str]: + lines: list[str] = [] + pending = "" + while time.time() < deadline: + chunk = ser.read(ser.in_waiting or 1) + if not chunk: + continue + pending += chunk.decode("utf-8", errors="ignore") + while "\n" in pending: + raw_line, pending = pending.split("\n", 1) + line = raw_line.rstrip("\r") + log_handle.write(line + "\n") + log_handle.flush() + lines.append(line) + if pending: + line = pending.rstrip("\r") + log_handle.write(line + "\n") + log_handle.flush() + lines.append(line) + return lines + + +def send_command(ser: serial.Serial, log_handle, command: str, wait_s: float) -> list[str]: + log_handle.write(host_line(f"SENT {command}") + "\n") + log_handle.flush() + ser.write((command + "\n").encode("utf-8")) + ser.flush() + return flush_lines(ser, log_handle, time.time() + wait_s) + + +def wait_for_command_channel(ser: serial.Serial, log_handle, timeout_s: float) -> bool: + deadline = time.time() + timeout_s + while time.time() < deadline: + flush_lines(ser, log_handle, time.time() + 0.35) + response = send_command(ser, log_handle, "PING", wait_s=1.2) + if any("PONG" in line or "UNKNOWN PING" in line for line in response): + log_handle.write(host_line("COMMAND CHANNEL READY") + "\n") + log_handle.flush() + return True + time.sleep(0.4) + log_handle.write(host_line("COMMAND CHANNEL NOT READY") + "\n") + log_handle.flush() + return False + + +def wait_for_quiet_period( + ser: serial.Serial, + log_handle, + timeout_s: float, + quiet_s: float, +) -> tuple[bool, float]: + deadline = time.time() + timeout_s + last_rx_at = time.time() + while time.time() < deadline: + lines = flush_lines(ser, log_handle, time.time() + 0.25) + if lines: + last_rx_at = time.time() + elif (time.time() - last_rx_at) >= quiet_s: + return True, last_rx_at + return False, last_rx_at + + +def main() -> int: + parser = argparse.ArgumentParser(description="Capture a rebooted ESP32 boot log over serial.") + parser.add_argument("--port", required=True) + parser.add_argument("--baud", type=int, default=115200) + parser.add_argument("--timeout", type=float, default=0.2) + parser.add_argument("--seconds", type=float, default=90.0) + parser.add_argument("--log", required=True) + args = parser.parse_args() + + log_path = Path(args.log) + log_path.parent.mkdir(parents=True, exist_ok=True) + + try: + with log_path.open("w", encoding="utf-8") as log_handle: + log_handle.write(host_line(f"OPEN port={args.port} baud={args.baud}") + "\n") + log_handle.flush() + + ser = open_serial(args.port, args.baud, args.timeout) + try: + ready = wait_for_command_channel(ser, log_handle, timeout_s=20.0) + if ready: + ser.close() + time.sleep(1.2) + ser = open_serial(args.port, args.baud, args.timeout) + log_handle.write(host_line("REOPEN FOR BOOT CAPTURE") + "\n") + log_handle.flush() + + capture_deadline = time.time() + args.seconds + boot_seen = False + post_boot_commands_sent = False + boot_seen_at = 0.0 + last_rx_at = time.time() + phase_start = time.time() + + while time.time() < capture_deadline: + lines = flush_lines(ser, log_handle, time.time() + 0.25) + if lines: + last_rx_at = time.time() + for line in lines: + if BOOT_BANNER in line: + boot_seen = True + boot_seen_at = time.time() + + should_probe = False + if ( + boot_seen + and not post_boot_commands_sent + and ( + (time.time() - last_rx_at) >= 1.8 + or (time.time() - boot_seen_at) >= 18.0 + ) + ): + should_probe = True + if ( + not boot_seen + and not post_boot_commands_sent + and (time.time() - phase_start) >= 18.0 + ): + should_probe = True + + if should_probe: + send_command(ser, log_handle, "PING", wait_s=2.0) + send_command(ser, log_handle, "STATUS", wait_s=2.4) + post_boot_commands_sent = True + + log_handle.write(host_line("CAPTURE COMPLETE") + "\n") + log_handle.flush() + finally: + try: + ser.close() + except Exception: + pass + except serial.SerialException as exc: + print(f"serial capture failed: {exc}", file=sys.stderr) + return 1 + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/specs/apps/IMPLEMENTATION_AUDIT_TODO.md b/specs/apps/IMPLEMENTATION_AUDIT_TODO.md new file mode 100644 index 0000000..08e8a0b --- /dev/null +++ b/specs/apps/IMPLEMENTATION_AUDIT_TODO.md @@ -0,0 +1,9 @@ +# Implementation Audit TODO + +Ce fichier sert de point d'ancrage pour les références historiques encore présentes dans le backlog. + +Les documents canoniques à consulter en priorité sont: + +- `docs/FNK0102H_SOURCE_OF_TRUTH.md` +- `docs/QUICKSTART.md` +- `README_ESP32_ZACUS.md` diff --git a/tests/phase6_test.py b/tests/phase6_test.py index 79e31e6..ff37b73 100644 --- a/tests/phase6_test.py +++ b/tests/phase6_test.py @@ -4,13 +4,15 @@ Phase 6: Hardware Validation - Interactive Serial Testing Tests P1/P2 security hardening on ESP32-S3 """ +import argparse +import os import serial import sys import time from typing import Optional class ESP32Tester: - def __init__(self, port: str = "/dev/cu.usbmodem5AB90753301", baudrate: int = 115200): + def __init__(self, port: str, baudrate: int = 115200): self.port = port self.baudrate = baudrate self.ser: Optional[serial.Serial] = None @@ -325,5 +327,12 @@ class ESP32Tester: if __name__ == "__main__": - tester = ESP32Tester() + parser = argparse.ArgumentParser(description="Phase 6 hardware validation over serial.") + parser.add_argument("--port", default=os.environ.get("ZACUS_SERIAL_PORT", "")) + parser.add_argument("--baud", type=int, default=int(os.environ.get("ZACUS_SERIAL_BAUD", "115200"))) + args = parser.parse_args() + if not args.port: + parser.error("--port or ZACUS_SERIAL_PORT is required") + + tester = ESP32Tester(port=args.port, baudrate=args.baud) tester.run_all_tests() diff --git a/tests/phase6_validation_smoke.sh b/tests/phase6_validation_smoke.sh index d48acd3..76b2ed2 100755 --- a/tests/phase6_validation_smoke.sh +++ b/tests/phase6_validation_smoke.sh @@ -3,15 +3,20 @@ ############################################################################### # Phase 6: Hardware Validation Smoke Tests # Tests P1/P2 security hardening on live ESP32-S3 device -# Port: /dev/cu.usbmodem5AB90753301 @ 115200 baud +# Port: pass as argument or use ZACUS_SERIAL_PORT ############################################################################### set -e -DEVICE_PORT="${1:-/dev/cu.usbmodem5AB90753301}" +DEVICE_PORT="${1:-${ZACUS_SERIAL_PORT:-}}" BAUD_RATE="115200" TEST_LOG="/tmp/phase6_test_results.log" +if [ -z "$DEVICE_PORT" ]; then + echo "Usage: $0 or set ZACUS_SERIAL_PORT" >&2 + exit 1 +fi + # Colors RED='\033[0;31m' GREEN='\033[0;32m' diff --git a/tests/phase9_ui_validation.py b/tests/phase9_ui_validation.py index 0bd0645..14fef7c 100644 --- a/tests/phase9_ui_validation.py +++ b/tests/phase9_ui_validation.py @@ -1,22 +1,61 @@ #!/usr/bin/env python3 """ -Phase 9: Device UI Validation & Touch Input Testing +Phase 9: Device UI Validation Tests: 1. Verify firmware boot & AmigaUIShell initialization 2. Monitor serial logs for UI animation frames 3. Validate system responsiveness (PING/STATUS) -4. Prepare for touch input mapping tests +4. Check repeated serial interactions for stability """ +import argparse +import os import serial -import time import sys -from pathlib import Path +import time # Configuration -SERIAL_PORT = "/dev/cu.usbmodem5AB90753301" -BAUD_RATE = 115200 -TIMEOUT = 2.0 +SERIAL_PORT = os.environ.get("ZACUS_SERIAL_PORT", "") +BAUD_RATE = int(os.environ.get("ZACUS_SERIAL_BAUD", "115200")) +TIMEOUT = float(os.environ.get("ZACUS_SERIAL_TIMEOUT", "2.0")) + + +def open_serial_port() -> serial.Serial: + ser = serial.Serial() + ser.port = SERIAL_PORT + ser.baudrate = BAUD_RATE + ser.timeout = TIMEOUT + ser.write_timeout = 1.0 + ser.dtr = False + ser.rts = False + ser.open() + time.sleep(0.25) + return ser + + +def wait_for_boot_settle(ser: serial.Serial, timeout_s: float = 45.0, quiet_s: float = 2.0) -> None: + deadline = time.time() + timeout_s + last_rx = time.time() + while time.time() < deadline: + if ser.in_waiting > 0: + _ = ser.read(ser.in_waiting) + last_rx = time.time() + elif (time.time() - last_rx) >= quiet_s: + return + time.sleep(0.04) + + +def send_command(ser: serial.Serial, command: str, wait_s: float = 2.0) -> str: + ser.reset_input_buffer() + ser.write((command + "\n").encode("utf-8")) + ser.flush() + deadline = time.time() + wait_s + chunks = [] + while time.time() < deadline: + if ser.in_waiting > 0: + chunks.append(ser.read(ser.in_waiting).decode("utf-8", errors="ignore")) + time.sleep(0.03) + return "".join(chunks) def test_device_responsive(): """Test 1: Device PING/STATUS""" @@ -25,15 +64,12 @@ def test_device_responsive(): print("="*60) try: - ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=TIMEOUT) - time.sleep(0.5) # Wait for serial to stabilize + ser = open_serial_port() + wait_for_boot_settle(ser) # Send PING print("[SERIAL] Sending: PING") - ser.write(b"PING\n") - time.sleep(0.2) - - response = ser.readline().decode('utf-8', errors='ignore').strip() + response = send_command(ser, "PING", wait_s=2.2).strip() if response: print(f"[RESPONSE] {response}") if "PONG" in response: @@ -45,17 +81,10 @@ def test_device_responsive(): # Send STATUS print("\n[SERIAL] Sending: STATUS") - ser.write(b"STATUS\n") - time.sleep(0.3) - - status_lines = [] - for _ in range(5): - line = ser.readline().decode('utf-8', errors='ignore').strip() - if line: - print(f"[STATUS] {line}") - status_lines.append(line) - if "scenario" in line.lower(): - break + raw_status = send_command(ser, "STATUS", wait_s=2.4) + status_lines = [line.strip() for line in raw_status.splitlines() if line.strip()] + for line in status_lines: + print(f"[STATUS] {line}") ser.close() return len(status_lines) > 0 @@ -71,8 +100,7 @@ def capture_startup_logs(): print("="*60) try: - ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=TIMEOUT) - time.sleep(0.5) + ser = open_serial_port() print("[SERIAL] Waiting for boot logs (10 seconds)...") print("-" * 60) @@ -116,8 +144,8 @@ def monitor_animation_frames(): print("="*60) try: - ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=TIMEOUT) - time.sleep(0.5) + ser = open_serial_port() + wait_for_boot_settle(ser) print("[SERIAL] Monitoring for animation tick messages (5 seconds)...") print("-" * 60) @@ -168,27 +196,23 @@ def check_memory_stability(): print("="*60) try: - ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=TIMEOUT) - time.sleep(0.5) + ser = open_serial_port() + wait_for_boot_settle(ser) # Trigger multiple HELP commands to stress test print("[SERIAL] Sending 5 HELP commands to monitor memory stability...") for i in range(5): - ser.write(b"HELP\n") - time.sleep(0.2) - - line = ser.readline(1024).decode('utf-8', errors='ignore').strip() - if "command" in line.lower(): + raw = send_command(ser, "HELP", wait_s=2.0) + line = raw.strip() + if "command" in line.lower() or "CMDS " in line: print(f" HELP #{i+1}: Command list received") # Send STATUS to check current memory print("\n[SERIAL] Final STATUS check...") - ser.write(b"STATUS\n") - time.sleep(0.3) - - for _ in range(5): - line = ser.readline(1024).decode('utf-8', errors='ignore').strip() + raw_status = send_command(ser, "STATUS", wait_s=2.4) + for line in raw_status.splitlines(): + line = line.strip() if line: print(f" {line}") @@ -201,8 +225,20 @@ def check_memory_stability(): return False def main(): + global SERIAL_PORT, BAUD_RATE, TIMEOUT + parser = argparse.ArgumentParser(description="Phase 9 UI validation over serial.") + parser.add_argument("--port", default=SERIAL_PORT) + parser.add_argument("--baud", type=int, default=BAUD_RATE) + parser.add_argument("--timeout", type=float, default=TIMEOUT) + args = parser.parse_args() + if not args.port: + parser.error("--port or ZACUS_SERIAL_PORT is required") + + SERIAL_PORT = args.port + BAUD_RATE = args.baud + TIMEOUT = args.timeout print("\n" + "█"*60) - print(" Phase 9: Device UI Validation & Touch Input Setup") + print(" Phase 9: Device UI Validation") print("█"*60) results = { diff --git a/tests/sprint1_utility_contract.py b/tests/sprint1_utility_contract.py index f9a82c2..04630b7 100644 --- a/tests/sprint1_utility_contract.py +++ b/tests/sprint1_utility_contract.py @@ -11,6 +11,7 @@ from __future__ import annotations import argparse import json +import os import re import sys import time @@ -100,11 +101,15 @@ class SerialRunner: self.ser: Optional[serial.Serial] = None def __enter__(self) -> "SerialRunner": - self.ser = serial.Serial(self.port, self.baud, timeout=self.timeout) + self.ser = serial.Serial() + self.ser.port = self.port + self.ser.baudrate = self.baud + self.ser.timeout = self.timeout + self.ser.write_timeout = 1.0 self.ser.dtr = False self.ser.rts = False - time.sleep(0.8) - self.ser.reset_input_buffer() + self.ser.open() + time.sleep(0.25) return self def __exit__(self, exc_type, exc, tb) -> None: @@ -127,7 +132,7 @@ class SerialRunner: time.sleep(0.03) return "".join(chunks) - def wait_for_boot_settle(self, timeout_s: float = 30.0, quiet_s: float = 1.6) -> None: + def wait_for_boot_settle(self, timeout_s: float = 45.0, quiet_s: float = 2.0) -> None: if self.ser is None: raise RuntimeError("serial connection closed") deadline = time.time() + timeout_s @@ -145,14 +150,14 @@ class SerialRunner: self.wait_for_boot_settle() last_raw = "" for _ in range(retries): - raw = self.send("PING", wait_s=1.6) + raw = self.send("PING", wait_s=2.2) if "PONG" in raw: return if "UNKNOWN PING" in raw: # Command path is active even if PING isn't mapped as expected. return last_raw = raw - time.sleep(0.25) + time.sleep(0.4) raise RuntimeError( "serial command channel unavailable after PING retries " f"(port={self.port}). Last output:\n{last_raw}" @@ -313,12 +318,14 @@ def main() -> int: parser = argparse.ArgumentParser(description="Sprint 1 utility contract + endurance checks.") parser.add_argument("--mode", choices=("serial", "http"), default="serial") parser.add_argument("--cycles", type=int, default=20) - parser.add_argument("--port", default="/dev/cu.usbmodem5AB90753301") + parser.add_argument("--port", default=os.environ.get("ZACUS_SERIAL_PORT", "")) parser.add_argument("--baud", type=int, default=115200) parser.add_argument("--timeout", type=float, default=2.0) - parser.add_argument("--base-url", default="http://192.168.4.1") - parser.add_argument("--token", default="") + parser.add_argument("--base-url", default=os.environ.get("ZACUS_BASE_URL", "http://192.168.4.1")) + parser.add_argument("--token", default=os.environ.get("ZACUS_WEB_TOKEN", "")) args = parser.parse_args() + if args.mode == "serial" and not args.port: + parser.error("--port or ZACUS_SERIAL_PORT is required in serial mode") started = time.time() try: diff --git a/tests/sprint2_capture_contract.py b/tests/sprint2_capture_contract.py index 8961e8c..d3b67a4 100644 --- a/tests/sprint2_capture_contract.py +++ b/tests/sprint2_capture_contract.py @@ -11,6 +11,7 @@ from __future__ import annotations import argparse import json +import os import re import sys import time @@ -100,11 +101,15 @@ class SerialRunner: self.ser: Optional[serial.Serial] = None def __enter__(self) -> "SerialRunner": - self.ser = serial.Serial(self.port, self.baud, timeout=self.timeout) + self.ser = serial.Serial() + self.ser.port = self.port + self.ser.baudrate = self.baud + self.ser.timeout = self.timeout + self.ser.write_timeout = 1.0 self.ser.dtr = False self.ser.rts = False - time.sleep(0.8) - self.ser.reset_input_buffer() + self.ser.open() + time.sleep(0.25) return self def __exit__(self, exc_type, exc, tb) -> None: @@ -127,7 +132,7 @@ class SerialRunner: time.sleep(0.03) return "".join(chunks) - def wait_for_boot_settle(self, timeout_s: float = 60.0, quiet_s: float = 3.5) -> None: + def wait_for_boot_settle(self, timeout_s: float = 60.0, quiet_s: float = 2.5) -> None: if self.ser is None: raise RuntimeError("serial connection closed") deadline = time.time() + timeout_s @@ -145,14 +150,14 @@ class SerialRunner: self.wait_for_boot_settle() last_raw = "" for _ in range(retries): - raw = self.send("PING", wait_s=1.6) + raw = self.send("PING", wait_s=2.2) if "PONG" in raw: return if "UNKNOWN PING" in raw: # Command path is active even if PING isn't mapped as expected. return last_raw = raw - time.sleep(0.25) + time.sleep(0.4) raise RuntimeError( "serial command channel unavailable after PING retries " f"(port={self.port}). Last output:\n{last_raw}" @@ -232,7 +237,7 @@ def run_serial_cycles(port: str, baud: int, timeout: float, cycles: int) -> None runner.wait_for_command_channel() for app_id in APPS: for cycle in range(1, cycles + 1): - open_out = runner.send(f"APP_OPEN {app_id} sprint2", wait_s=0.9) + open_out = runner.send(f"APP_OPEN {app_id} sprint2", wait_s=2.4) if "ACK APP_OPEN ok=1" not in open_out: fallback = runner.status() if fallback.state not in ("running", "starting") or ( @@ -329,12 +334,14 @@ def main() -> int: parser = argparse.ArgumentParser(description="Sprint 2 capture contract + endurance checks.") parser.add_argument("--mode", choices=("serial", "http"), default="serial") parser.add_argument("--cycles", type=int, default=10) - parser.add_argument("--port", default="/dev/cu.usbmodem5AB90753301") + parser.add_argument("--port", default=os.environ.get("ZACUS_SERIAL_PORT", "")) parser.add_argument("--baud", type=int, default=115200) parser.add_argument("--timeout", type=float, default=2.0) - parser.add_argument("--base-url", default="http://192.168.4.1") - parser.add_argument("--token", default="") + parser.add_argument("--base-url", default=os.environ.get("ZACUS_BASE_URL", "http://192.168.4.1")) + parser.add_argument("--token", default=os.environ.get("ZACUS_WEB_TOKEN", "")) args = parser.parse_args() + if args.mode == "serial" and not args.port: + parser.error("--port or ZACUS_SERIAL_PORT is required in serial mode") started = time.time() try: diff --git a/tests/sprint3_audio_contract.py b/tests/sprint3_audio_contract.py new file mode 100755 index 0000000..0e0f01f --- /dev/null +++ b/tests/sprint3_audio_contract.py @@ -0,0 +1,347 @@ +#!/usr/bin/env python3 +""" +Sprint 3 audio contract + endurance checks (audio_player, audiobook_player, kids_music). + +Modes: +- Serial: APP_OPEN / APP_ACTION / APP_STATUS / APP_CLOSE +- HTTP: /api/apps/open|action|status|close +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +import time +from dataclasses import dataclass +from typing import Dict, List, Optional, Sequence, Tuple + +import requests + +try: + import serial +except Exception: # pragma: no cover - optional until serial mode is used + serial = None + + +APPS: Sequence[str] = ("audio_player", "audiobook_player", "kids_music") + +# (action_name, payload) +APP_ACTIONS: Dict[str, Sequence[Tuple[str, str]]] = { + "audio_player": ( + ("play", "/apps/audio_player/audio/default.mp3"), + ("pause", ""), + ("resume", ""), + ("set_volume", "12"), + ("stop", ""), + ), + "audiobook_player": ( + ("open_book", "/apps/audiobook_player/audio/default.mp3"), + ("play", ""), + ("seek_ms", "12000"), + ("bookmark_set", "12000"), + ("bookmark_go", ""), + ("pause", ""), + ("stop", ""), + ), + "kids_music": ( + ("play", "/apps/kids_music/audio/default.mp3"), + ("pause", ""), + ("resume", ""), + ("stop", ""), + ), +} + + +@dataclass +class Status: + app_id: str + state: str + mode: str + source: str + last_error: str + last_event: str + + +APP_STATUS_RE = re.compile( + r"APP_STATUS id=(?P\S*) state=(?P\S+) mode=(?P\S+) " + r"source=(?P\S+) err=(?P.*?) event=(?P.*?) tick=(?P\d+) " + r"missing=(?P0x[0-9A-Fa-f]+)" +) + + +def parse_serial_status(raw: str) -> Optional[Status]: + for line in reversed(raw.splitlines()): + if "APP_STATUS " not in line: + continue + match = APP_STATUS_RE.search(line.strip()) + if match is None: + continue + return Status( + app_id=match.group("id"), + state=match.group("state"), + mode=match.group("mode"), + source=match.group("source"), + last_error=match.group("err"), + last_event=match.group("event"), + ) + return None + + +class SerialRunner: + def __init__(self, port: str, baud: int, timeout: float) -> None: + if serial is None: + raise RuntimeError("pyserial is required for serial mode") + self.port = port + self.baud = baud + self.timeout = timeout + self.ser: Optional[serial.Serial] = None + + def __enter__(self) -> "SerialRunner": + self.ser = serial.Serial() + self.ser.port = self.port + self.ser.baudrate = self.baud + self.ser.timeout = self.timeout + self.ser.write_timeout = 1.0 + self.ser.dtr = False + self.ser.rts = False + self.ser.open() + time.sleep(0.25) + return self + + def __exit__(self, exc_type, exc, tb) -> None: + if self.ser is not None: + self.ser.close() + self.ser = None + + def send(self, command: str, wait_s: float = 1.2) -> str: + if self.ser is None: + raise RuntimeError("serial connection closed") + self.ser.reset_input_buffer() + self.ser.write((command + "\r\n").encode("utf-8")) + self.ser.flush() + deadline = time.time() + wait_s + chunks: List[str] = [] + while time.time() < deadline: + if self.ser.in_waiting > 0: + data = self.ser.read(self.ser.in_waiting).decode("utf-8", errors="ignore") + chunks.append(data) + time.sleep(0.03) + return "".join(chunks) + + def wait_for_boot_settle(self, timeout_s: float = 45.0, quiet_s: float = 2.0) -> None: + if self.ser is None: + raise RuntimeError("serial connection closed") + deadline = time.time() + timeout_s + last_rx = time.time() + while time.time() < deadline: + if self.ser.in_waiting > 0: + _ = self.ser.read(self.ser.in_waiting) + last_rx = time.time() + elif (time.time() - last_rx) >= quiet_s: + return + time.sleep(0.04) + + def wait_for_command_channel(self, retries: int = 8) -> None: + self.wait_for_boot_settle() + last_raw = "" + for _ in range(retries): + raw = self.send("PING", wait_s=2.2) + if "PONG" in raw or "UNKNOWN PING" in raw: + return + last_raw = raw + time.sleep(0.4) + raise RuntimeError( + "serial command channel unavailable after PING retries " + f"(port={self.port}). Last output:\n{last_raw}" + ) + + def status(self) -> Status: + last_raw = "" + for _ in range(4): + raw = self.send("APP_STATUS", wait_s=1.4) + parsed = parse_serial_status(raw) + if parsed is not None: + return parsed + last_raw = raw + time.sleep(0.25) + raise RuntimeError(f"APP_STATUS parse failed. Raw output:\n{last_raw}") + + +class ApiRunner: + def __init__(self, base_url: str, token: str) -> None: + self.base_url = base_url.rstrip("/") + self.session = requests.Session() + if token: + self.session.headers["Authorization"] = f"Bearer {token}" + self.session.headers["Content-Type"] = "application/json" + + def post(self, path: str, payload: dict) -> dict: + resp = self.session.post(f"{self.base_url}{path}", data=json.dumps(payload), timeout=8) + resp.raise_for_status() + return resp.json() + + def get(self, path: str) -> dict: + resp = self.session.get(f"{self.base_url}{path}", timeout=8) + resp.raise_for_status() + return resp.json() + + def status(self) -> Status: + body = self.get("/api/apps/status") + runtime = body.get("runtime", {}) + return Status( + app_id=str(runtime.get("running_id", "")), + state=str(runtime.get("state", "")), + mode=str(runtime.get("mode", "")), + source=str(runtime.get("source", "")), + last_error=str(runtime.get("last_error", "")), + last_event=str(runtime.get("last_event", "")), + ) + + def wait_until_ready(self, timeout_s: float = 45.0) -> None: + deadline = time.time() + timeout_s + last_error = "" + while time.time() < deadline: + try: + self.get("/api/apps/status") + return + except Exception as exc: # pragma: no cover - network/device dependent + last_error = str(exc) + time.sleep(1.0) + raise RuntimeError( + f"API unreachable at {self.base_url} after {timeout_s:.0f}s (last_error={last_error})" + ) + + +def assert_status_running(status: Status, app_id: str) -> None: + if status.state not in ("running", "starting"): + raise RuntimeError(f"{app_id}: unexpected state after open: {status.state}") + if status.app_id and status.app_id != app_id: + raise RuntimeError(f"{app_id}: running_id mismatch: got={status.app_id}") + + +def assert_status_idle(status: Status) -> None: + if status.state != "idle": + raise RuntimeError(f"expected idle after close, got={status.state}") + + +def run_serial_cycles(port: str, baud: int, timeout: float, cycles: int) -> None: + with SerialRunner(port=port, baud=baud, timeout=timeout) as runner: + runner.wait_for_command_channel() + for app_id in APPS: + for cycle in range(1, cycles + 1): + open_out = runner.send(f"APP_OPEN {app_id} sprint3", wait_s=0.9) + if "ACK APP_OPEN ok=1" not in open_out: + fallback = runner.status() + if fallback.state not in ("running", "starting") or ( + fallback.app_id and fallback.app_id != app_id + ): + raise RuntimeError(f"{app_id} cycle#{cycle}: APP_OPEN failed\n{open_out}") + + st = runner.status() + assert_status_running(st, app_id) + + for action, payload in APP_ACTIONS[app_id]: + cmd = f"APP_ACTION {action}" + (f" {payload}" if payload else "") + action_out = runner.send(cmd, wait_s=0.9) + if "ACK APP_ACTION ok=1" not in action_out: + st_mid = runner.status() + if st_mid.state not in ("running", "starting"): + raise RuntimeError( + f"{app_id} cycle#{cycle}: APP_ACTION failed ({cmd})\n{action_out}" + ) + time.sleep(0.1 if action in ("play", "resume") else 0.05) + + st_after = runner.status() + if st_after.last_error not in ("", "none"): + raise RuntimeError( + f"{app_id} cycle#{cycle}: runtime error={st_after.last_error} " + f"event={st_after.last_event}" + ) + + close_out = runner.send("APP_CLOSE sprint3_cycle", wait_s=0.9) + if "ACK APP_CLOSE ok=1" not in close_out: + st_close = runner.status() + if st_close.state != "idle": + raise RuntimeError(f"{app_id} cycle#{cycle}: APP_CLOSE failed\n{close_out}") + st_idle = runner.status() + assert_status_idle(st_idle) + + print(f"[SERIAL] {app_id} cycle {cycle}/{cycles} OK") + + +def run_api_cycles(base_url: str, token: str, cycles: int) -> None: + runner = ApiRunner(base_url=base_url, token=token) + runner.wait_until_ready() + for app_id in APPS: + for cycle in range(1, cycles + 1): + opened = runner.post( + "/api/apps/open", + {"id": app_id, "mode": "sprint3", "source": "sprint3_test"}, + ) + if not opened.get("ok", False): + raise RuntimeError(f"{app_id} cycle#{cycle}: /open failed {opened}") + + st = runner.status() + assert_status_running(st, app_id) + + for action, payload in APP_ACTIONS[app_id]: + payload_json = { + "id": app_id, + "action": action, + "payload": payload, + "content_type": "text/plain", + } + acted = runner.post("/api/apps/action", payload_json) + if not acted.get("ok", False): + raise RuntimeError(f"{app_id} cycle#{cycle}: /action failed {acted}") + time.sleep(0.1 if action in ("play", "resume") else 0.05) + + st_after = runner.status() + if st_after.last_error not in ("", "none"): + raise RuntimeError( + f"{app_id} cycle#{cycle}: runtime error={st_after.last_error} " + f"event={st_after.last_event}" + ) + + closed = runner.post("/api/apps/close", {"id": app_id, "reason": "sprint3_cycle"}) + if not closed.get("ok", False): + raise RuntimeError(f"{app_id} cycle#{cycle}: /close failed {closed}") + st_idle = runner.status() + assert_status_idle(st_idle) + + print(f"[HTTP] {app_id} cycle {cycle}/{cycles} OK") + + +def main() -> int: + parser = argparse.ArgumentParser(description="Sprint 3 audio contract + endurance checks.") + parser.add_argument("--mode", choices=("serial", "http"), default="serial") + parser.add_argument("--cycles", type=int, default=10) + parser.add_argument("--port", default=os.environ.get("ZACUS_SERIAL_PORT", "")) + parser.add_argument("--baud", type=int, default=115200) + parser.add_argument("--timeout", type=float, default=2.0) + parser.add_argument("--base-url", default=os.environ.get("ZACUS_BASE_URL", "http://192.168.4.1")) + parser.add_argument("--token", default=os.environ.get("ZACUS_WEB_TOKEN", "")) + args = parser.parse_args() + if args.mode == "serial" and not args.port: + parser.error("--port or ZACUS_SERIAL_PORT is required in serial mode") + + started = time.time() + try: + if args.mode == "serial": + run_serial_cycles(args.port, args.baud, args.timeout, args.cycles) + else: + run_api_cycles(args.base_url, args.token, args.cycles) + except Exception as exc: + print(f"FAILED: {exc}") + return 1 + + elapsed = time.time() - started + print(f"SUCCESS: sprint3 audio cycles completed ({args.cycles} cycles/app) in {elapsed:.1f}s") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/ui_freenove_allinone/RC_FINAL_BOARD.md b/ui_freenove_allinone/RC_FINAL_BOARD.md index a2571b3..b5745a4 100644 --- a/ui_freenove_allinone/RC_FINAL_BOARD.md +++ b/ui_freenove_allinone/RC_FINAL_BOARD.md @@ -1,6 +1,6 @@ # Multiplexage des broches – Attention -Cible matérielle : **ESP32-S3-WROOM-1-N16R8**. +Cible matérielle : **Freenove ESP32-S3 WROOM N16R8**. Certaines broches sont partagées entre plusieurs fonctions : - GPIO 4 : utilisé pour le rétroéclairage (BL) et le bouton 3 (BTN3) diff --git a/ui_freenove_allinone/README.md b/ui_freenove_allinone/README.md index d99c715..042de55 100644 --- a/ui_freenove_allinone/README.md +++ b/ui_freenove_allinone/README.md @@ -1,357 +1,53 @@ +# ui_freenove_allinone ---- -# 🎛️ Zacus Firmware – Freenove All-in-One +Firmware principal Freenove pour `ESP32_ZACUS`. -![Funk](https://media.giphy.com/media/3o7TKtnuHOHHUjR38Y/giphy.gif) +## Profil matériel release ---- +- Carte: Freenove FNK0102H. +- Board profile PlatformIO: `freenove_esp32_s3_wroom` (`N16R8`, 16 MB flash / 8 MB PSRAM). +- Écran: ST7796 320x480, rotation `1`. +- Entrée officielle: switch analogique 5 directions sur GPIO19. +- Touch: non supporté dans le chemin release. -## 📝 Description +## Mapping principal -Bienvenue dans le cockpit le plus funky du multivers ! Ce firmware fusionne UI, audio, scénarios et hardware sur la carte Freenove Media Kit (ESP32). Ici, chaque pixel danse, chaque bouton groove, et chaque boot est une fête. +| Fonction | GPIO | +| --- | --- | +| TFT SCK | 47 | +| TFT MOSI | 21 | +| TFT DC | 45 | +| TFT RESET | 20 | +| TFT BL | 2 | +| Boutons analogiques | 19 | +| SD CMD / CLK / D0 | 38 / 39 / 40 | +| I2S WS / BCK / DOUT | 41 / 42 / 1 | +| Caméra XCLK / SIOD / SIOC | 15 / 4 / 5 | -> *"Si tu entends un son rétro ou vois un plasma violet, c’est normal. Si le microcontrôleur se met à rapper, c’est probablement un bug… ou un feature caché."* - ---- - -## 🕹️ Mapping hardware (ESP32-S3 Freenove Media Kit) - -| Fonction | Broche ESP32-S3 | Signal TFT/Touch/Audio | Remarques | -|------------------|-----------------|-----------------------|--------------------------------| -| TFT SCK | GPIO 47 | SCK | SPI écran (FNK0102B) | -| TFT MOSI | GPIO 21 | MOSI | SPI écran | -| TFT MISO | -1 | MISO | non utilisé | -| TFT CS | -1 | CS | câblage board intégré | -| TFT DC | GPIO 45 | DC | Data/Command écran | -| TFT RESET | GPIO 20 | RESET | Reset écran | -| TFT BL | GPIO 2 | BL | Rétroéclairage | -| Touch CS | GPIO 9 | CS (XPT2046) | optionnel (`FREENOVE_HAS_TOUCH`) | -| Touch IRQ | GPIO 15 | IRQ (XPT2046) | optionnel | -| Boutons | GPIO 19 | ADC ladder (5 touches)| key1..key5 par seuils analogiques | -| Audio I2S WS | GPIO 41 | WS | profil principal Sketch_19 | -| Audio I2S BCK | GPIO 42 | BCK | profil principal Sketch_19 | -| Audio I2S DOUT | GPIO 1 | DOUT | profil principal Sketch_19 | -| Alim écran/audio | 3V3/5V/GND | - | Respecter les tensions | - -**Remarques** : -- Profil audio runtime sélectionnable par série: `AUDIO_PROFILE ` puis `AUDIO_TEST`. -- Profils fournis: `0=sketch19`, `1=swap_bck_ws`, `2=dout2_alt`. -- Le tactile est désactivé par défaut (`FREENOVE_HAS_TOUCH=0`). - ---- - -## 📦 Contenu du dossier - -- Sources : `src/` (app, ui, audio, storage, camera, drivers, system) -- Polices : `src/ui/fonts/` -- Layouts d’écran : `src/ui/screens/` -- Partition custom : `partitions/freenove_esp32s3_app6mb_fs6mb.csv` -- Mapping hardware détaillé (voir pour descendre plus haut ) - ---- - -## 🚀 Installation & usage - -1. Clone ce repo, chausse tes lunettes de soleil. -2. Va dans `hardware/firmware` et build comme un DJ : - ```sh - pio run -e freenove_allinone - pio run -e freenove_allinone -t upload --upload-port - ``` -3. Pour la version ESP32-S3 (partition 6MB app / 6MB FS) : - ```sh - pio run -e freenove_esp32s3 - pio run -e freenove_esp32s3 -t buildfs - pio run -e freenove_esp32s3 -t uploadfs --upload-port - pio run -e freenove_esp32s3 -t upload --upload-port - ``` -4. Branche, allume, et laisse la magie opérer. - ---- - -## 🧩 Fonctionnalités qui groovent - -- Navigation UI dynamique (LVGL, écrans générés depuis fichiers) -- Exécution de scénarios (lecture, transitions, actions, audio) -- Gestion audio (lecture/stop, mapping fichiers LittleFS) -- Gestion boutons et tactile (événements, mapping, callbacks) -- Fallback robuste si fichier manquant (scénario par défaut) -- Génération de logs et artefacts (logs/, artifacts/) -- Validation hardware sur Freenove (affichage, audio, boutons, tactile) -- Documentation et onboarding synchronisés -- Mode autonome (pas besoin d’ESP32 séparé) - -> *"Si tu rates un bouton, c’est que tu danses trop fort. Si tu vois un plasma violet, c’est que tu es dans le groove."* - ---- - -## 🛠️ Modules principaux - -- `audio_manager` : fait swinguer l’audio (lecture, stop, état) -- `scenario_manager` : enchaîne les étapes comme un DJ -- `ui_manager` : LVGL, écrans dynamiques, FX visuels -- `storage_manager` : LittleFS (init, vérif, groove des assets) -- `button_manager` : boutons physiques, pour les vrais -- `touch_manager` : tactile XPT2046, pour les DJ du futur - ---- - -## 🦄 Funky extras & notes - -- Firmware expérimental : fusion audio + UI, mode "party hard" activé -- Pour la compatibilité UI Link, prévoir un mode optionnel -- Si tu veux une intro Amiga92, active la scène `SCENE_WIN_ETAPE` et laisse-toi porter par le FX timeline (plasma, starfield, boingball…) -- Pour tester la scène MP3 : `SCENE_MP3_PLAYER` (overlay AmigaAMP, scan `/music`) -- Pour la caméra : `SCENE_CAMERA_SCAN` (Win311 overlay, boutons = SNAP/SAVE/GALLERY/DELETE/CLOSE) -- Pour tout le reste, consulte les logs, et si tu comprends tout du premier coup, tu gagnes un badge "Zacus Funk Master". - ---- - -## 🤝 Contribuer - -Merci de lire [../../../../../../CONTRIBUTING.md](../../../../../../CONTRIBUTING.md) avant toute PR. Les pull requests avec punchlines sont encouragées. - ---- - -## 👤 Contact - -Pour toute question, suggestion, ou battle de funk, ouvre une issue GitHub ou contacte l’auteur principal : -- Clément SAILLANT — [github.com/electron-rare](https://github.com/electron-rare) - -> *"Ce firmware a été validé par un oscilloscope, un grille-pain, et un synthé vintage. Si tu trouves un bug, c’est peut-être un easter egg ou feat foirée."* ---- - -# Firmware Freenove Media Kit All-in-One - -## Plan d’intégration complète (couverture specs) - -- Fichiers de scènes et écrans individuels, stockés sur LittleFS (data/) -- Navigation UI dynamique (LVGL, écrans générés depuis fichiers) -- Exécution de scénarios (lecture, transitions, actions, audio) -- Gestion audio (lecture/stop, mapping fichiers LittleFS) -- Gestion boutons et tactile (événements, mapping, callbacks) -- Fallback robuste si fichier manquant (scénario par défaut) -- Génération de logs et artefacts (logs/, artifacts/) -- Validation hardware sur Freenove (affichage, audio, boutons, tactile) -- Documentation et onboarding synchronisés - -## Structure modulaire - -- `src/app/*` : orchestration runtime (`main`, `scenario_manager`, coordinator serie) -- `src/ui/*` : LVGL scenes, fonts, FX helpers (`ui_manager`, `ui_fonts`, `ui/fx`) -- `src/audio/*` : gestion audio runtime (`audio_manager`) -- `src/storage/*` : LittleFS/SD et resolution assets (`storage_manager`) -- `src/camera/*` : camera runtime (`camera_manager`) -- `src/drivers/*` : board I/O (input, board, display HAL + SPI bus manager) -- `src/system/*` : metrics, boot report, reseau/media wrappers, task topology -- headers legacy (`include/*_manager.h`) conserves en facades vers les headers modulaires - -Ce firmware combine : -- Les fonctions audio/scénario (type ESP32 Audio) -- L’UI locale (affichage, interaction, tactile, boutons) -- Le tout sur un seul microcontrôleur (RP2040 ou ESP32 selon le kit) - - -## Fonctionnalités -- Lecture audio, gestion scénario, LittleFS -- Affichage TFT tactile (LVGL) -- Boutons physiques, capteurs, extensions -- Mode autonome (pas besoin d’ESP32 séparé) - -## Modules principaux -- audio_manager : gestion audio (lecture, stop, état) -- scenario_manager : gestion scénario (étapes, transitions) -- ui_manager : gestion UI (LVGL, écrans dynamiques) -- storage_manager : gestion LittleFS (init, vérification) -- button_manager : gestion boutons physiques -- touch_manager : gestion tactile XPT2046 - -## Validation hardware -- Compiler et flasher sur le Freenove Media Kit -- Vérifier l’affichage, la réactivité tactile et boutons -- Tester la lecture audio (fichiers dans /data/) -- Consulter les logs série pour le suivi d’exécution - -## Artefacts -- Firmware compilé (.bin) -- Logs de test hardware (logs/) -- Rapport de compatibilité assets LittleFS +Le détail complet est maintenu dans `../docs/FNK0102H_SOURCE_OF_TRUTH.md`. ## Build -Depuis `hardware/firmware` : +Depuis la racine du repo: -```sh -pio run -e freenove_allinone -pio run -e freenove_allinone -t upload --upload-port +```bash +source .venv/bin/activate +python -m platformio run -e freenove_esp32s3_full_with_ui +python -m platformio run -e freenove_esp32s3_full_with_ui -t buildfs +python -m platformio run -e freenove_esp32s3_full_with_ui -t uploadfs --upload-port +python -m platformio run -e freenove_esp32s3_full_with_ui -t upload --upload-port ``` -## Build Freenove ESP32-S3 (partition 6MB app / 6MB FS) +## Boot modes -```sh -pio run -e freenove_esp32s3 -pio run -e freenove_esp32s3 -t buildfs -pio run -e freenove_esp32s3 -t uploadfs --upload-port -pio run -e freenove_esp32s3 -t upload --upload-port +- Normal: UI complète, caméra, audio et réseau autorisés. +- `safe diagnostic mode`: affichage minimal + boutons + série uniquement si la PSRAM n'est pas détectée. + +## Tests + +```bash +python tests/sprint1_utility_contract.py --mode serial --port +python tests/sprint2_capture_contract.py --mode serial --port +python tests/sprint3_audio_contract.py --mode serial --port +python tests/phase9_ui_validation.py --port ``` - -- Partition custom active: `partitions/freenove_esp32s3_app6mb_fs6mb.csv`. -- Le bundle story complet n'est plus embarque dans le firmware: `buildfs/uploadfs` est requis pour charger le contenu complet (screens/audio/actions/apps). -- Fallback embarque minimal conserve: `APP_WIFI` + scenario `DEFAULT` minimal. - -## Norme embarquee (audit + refacto) - -- Standard projet: `docs/skills/EMBEDDED_CPP_OO_ESP32S3_PIO_ARDUINO.md`. -- Toute revue technique Freenove doit citer cette norme pour les risques temps reel, memoire, concurrence et style OO. - -## Provisioning Wi-Fi + Auth WebUI - -- Au boot sans credentials NVS: - - mode setup AP actif; - - endpoints setup ouverts: `GET /api/provision/status`, `POST /api/wifi/connect`, `POST /api/network/wifi/connect`. -- Hors setup: - - auth Bearer requise sur `/api/*`: - - `Authorization: Bearer `. -- Provisioning persistant: - - API: `POST /api/wifi/connect` (ou `/api/network/wifi/connect`) avec `persist=1`. - - Serie: `WIFI_PROVISION `. -- Rotation token: - - Serie: `AUTH_TOKEN_ROTATE [token]`. -- Outils shell: - - `tools/dev/healthcheck_wifi.sh` et `tools/dev/rtos_wifi_health.sh` supportent `ZACUS_WEB_TOKEN`. - -## LVGL graphics stack (ESP32-S3) - -- Le runtime Freenove (`env:freenove_esp32s3*`) supporte: - - `LV_COLOR_DEPTH=8` (RGB332) avec conversion RGB565 au flush. - - draw buffers lignes en double-buffer. - - flush DMA asynchrone (overlap draw/transfert) avec fallback sync. -- Flags principaux (`platformio.ini`): - - `UI_COLOR_256`, `UI_COLOR_565`, `UI_FORCE_THEME_256` - - `UI_DRAW_BUF_LINES`, `UI_DRAW_BUF_IN_PSRAM` - - `UI_DMA_FLUSH_ASYNC`, `UI_DMA_TRANS_BUF_LINES` - - `UI_FULL_FRAME_BENCH`, `UI_LV_MEM_SIZE_KB` -- Commandes debug serie: - - `UI_GFX_STATUS` - - `UI_MEM_STATUS` -- Documentation associee: - - `docs/ui/graphics_stack.md` - - `docs/ui/lvgl_memory_budget.md` - - `docs/ui/fonts_fr.md` - -## Mapping hardware -- Voir `include/ui_freenove_config.h` pour l’adaptation des pins -- Schéma de branchement : se référer à la doc Freenove - -## Notes -- Ce firmware est expérimental et fusionne les logiques audio + UI. -- Pour la compatibilité UI Link, prévoir un mode optionnel. - -## Intro Amiga92 (`SCENE_WIN_ETAPE`) - -- Activation: l'intro A/B/C est lancee automatiquement quand `screen_scene_id == SCENE_WIN_ETAPE`. -- Mode runtime: `FX_ONLY_V9` avec rendu timeline FX (plasma/starfield/rasterbars + tunnel3d/rotozoom/wirecube + boingball) et overlay LVGL conserve. -- Sequence timeline verrouillee: `A(30000ms) -> B(15000ms) -> C(20000ms) -> C loop`. -- Mapping presets (default): - - A: `demo` - - B: `winner` - - C: `boingball` -- Font scroller default: `italic`. -- BPM default: `125`. -- Boing shadow path: assembleur S3 active par defaut (`UI_BOING_SHADOW_ASM=1`) avec fallback C automatique. -- Log de boot FX: `boing_shadow_path=asm|c`. - -### Overrides runtime (TXT + JSON) - -- Priorite de lecture: - 1) `/ui/scene_win_etape.json` - 2) `/SCENE_WIN_ETAPE.json` - 3) `/ui/SCENE_WIN_ETAPE.json` - 4) `/ui/scene_win_etape.txt` -- Cles supportees: - - `A_MS`, `B_MS`, `C_MS` - - `FX_PRESET_A`, `FX_PRESET_B`, `FX_PRESET_C` (`demo|winner|fireworks|boingball`) - - `FX_MODE_A`, `FX_MODE_B`, `FX_MODE_C` (`classic|starfield3d|dotsphere3d|voxel|raycorridor`) - - `FX_SCROLL_TEXT_A`, `FX_SCROLL_TEXT_B`, `FX_SCROLL_TEXT_C` - - `FX_SCROLL_FONT` (`basic|bold|outline|italic`) - - `FX_BPM` (`60..220`) -- Rupture de compatibilite volontaire: - - anciennes cles FX `FX_3D`, `FX_3D_QUALITY`, `FONT_MODE` ne sont plus prises en charge dans ce flux. - -Exemples: -- JSON: `data/SCENE_WIN_ETAPE.json` -- TXT: `data/ui/scene_win_etape.txt` - -### Perf notes - -- Tick fixe: `42 ms` (`~24 FPS` cible), `dt` clamp pour robustesse. -- Zero allocation par frame dans la boucle intro (`tickIntro`). -- Blit LGFX: fast-path 2x active (`UI_FX_BLIT_FAST_2X=1`) si ratio source/ecran exact, fallback scaler general sinon. -- Caps objets scene: `<=140` (petit ecran), `<=260` (grand ecran). -- Pools fixes: - - stars: `48` - - fireworks: `72` - - wave glyph+shadow: `64 + 64` -- `UI_GFX_STATUS` expose `fx_fps/fx_frames/fx_skip_busy` + compteurs `flush_block/overflow` pour diagnostiquer les saccades LVGL/FX. - -## Scenes demoscene exposees - -- IDs canoniques ajoutes: `SCENE_WINNER`, `SCENE_FIREWORKS`. -- Fichiers data: - - `data/story/screens/SCENE_WINNER.json` - - `data/story/screens/SCENE_FIREWORKS.json` -- Registry story: `hardware/libs/story/src/resources/screen_scene_registry.cpp`. - -## Scene lecteur MP3 (`SCENE_MP3_PLAYER`) - -- Scene canonique: `SCENE_MP3_PLAYER`. -- Aliases supportes: `SCENE_AUDIO_PLAYER`, `SCENE_MP3`. -- Data scene: `data/story/screens/SCENE_MP3_PLAYER.json`. -- Mode runtime: overlay LVGL "AmigaAMP" + backend `AudioPlayerService` (scan `/music`, fallback `/audio/music`, fallback `/audio`). -- Arbitrage audio: - - en scene MP3, l'audio scenario est suspendu; - - en sortie de scene MP3, pipeline audio scenario restaure. -- Commandes serie: - - `AMP_SHOW`, `AMP_HIDE`, `AMP_TOGGLE` - - `AMP_SCAN`, `AMP_PLAY `, `AMP_NEXT`, `AMP_PREV`, `AMP_STOP`, `AMP_STATUS` - -## Scene camera recorder (`SCENE_CAMERA_SCAN`) - -- Binding scene: entree `SCENE_CAMERA_SCAN` -> session recorder camera active (`RGB565/QVGA`) + overlay Win311 visible. -- Sortie scene: overlay masque, frame gelee purgee, retour mode snapshot legacy (`JPEG`). -- Mapping boutons physiques: - - `BTN1`: `SNAP/LIVE` - - `BTN2`: `SAVE` - - `BTN3`: `GALLERY` (appui long: `NEXT`) - - `BTN4`: `DELETE` - - `BTN5`: `CLOSE` overlay -- Commandes serie: - - `CAM_UI_SHOW`, `CAM_UI_HIDE`, `CAM_UI_TOGGLE` - - `CAM_REC_SNAP`, `CAM_REC_SAVE [auto|bmp|jpg|raw]` - - `CAM_REC_GALLERY`, `CAM_REC_NEXT`, `CAM_REC_DELETE`, `CAM_REC_STATUS` -- Ownership runtime: - - pendant `SCENE_CAMERA_SCAN`, les boutons physiques ne sont pas forwardes au scenario; - - commandes legacy `CAM_ON/CAM_OFF/CAM_SNAPSHOT` renvoient `camera_busy_recorder_owner`. - -### Visual verification mode - -- Build flags (env `freenove_esp32s3`) pour tests scene: - - `UI_FULL_FRAME_BENCH=1` - - `UI_DEMO_AUTORUN_WIN_ETAPE=1` -- Quand ces flags sont actifs: - - la scene `SCENE_WIN_ETAPE` demarre automatiquement au boot; - - la phase C continue en boucle tant que la scene reste active; - - `UI_GFX_STATUS` permet de verifier le mode runtime (depth/full_frame/source). - -### References consulted - -- https://www.pouet.net/prodlist.php?type%5B0%5D=cracktro -- https://www.youtube.com/results?search_query=amiga+cracktro -- https://www.youtube.com/results?search_query=amiga+demoscene+1992 -- https://www.theflatnet.de/pub/cbm/amiga/AmigaDevDocs/hard_2.html -- https://www.theflatnet.de/pub/cbm/amiga/AmigaDevDocs/ -- https://www.markwrobel.dk/project/amigamachinecode/ -- https://www.markwrobel.dk/post/amiga-machine-code-letter3-copper-revisited/ -- https://www.markwrobel.dk/post/amiga-machine-code-letter12-starfield-effect/ -- https://www.markwrobel.dk/post/amiga-machine-code-letter12-wave/ -- https://github.com/mrandreastoth/AmigaStyleDemo diff --git a/ui_freenove_allinone/include/app/app_module.h b/ui_freenove_allinone/include/app/app_module.h new file mode 100644 index 0000000..59fede7 --- /dev/null +++ b/ui_freenove_allinone/include/app/app_module.h @@ -0,0 +1,39 @@ +// app_module.h - Interface for pluggable app modules. +#pragma once + +#include + +struct RuntimeServices; +struct AppEntry; + +enum class AppModuleState : uint8_t { + kIdle = 0, + kStarting, + kRunning, + kError, +}; + +class IAppModule { + public: + virtual ~IAppModule() = default; + + virtual const char* id() const = 0; + + // Called once when APP_OPEN arrives. Return true if launch succeeded. + virtual bool onOpen(const AppEntry& entry, const char* mode, RuntimeServices* services) = 0; + + // Called once when APP_CLOSE arrives. + virtual void onClose(const char* reason, RuntimeServices* services) = 0; + + // Called every frame while the app is running. + virtual void onTick(uint32_t now_ms, RuntimeServices* services) = 0; + + // Dispatch an APP_ACTION. Return true if handled. + virtual bool onAction(const char* action, const char* payload, RuntimeServices* services) = 0; + + // Current module state. + virtual AppModuleState state() const = 0; + + // Last error (may be empty). + virtual const char* lastError() const { return ""; } +}; diff --git a/ui_freenove_allinone/include/app/app_registry.h b/ui_freenove_allinone/include/app/app_registry.h index 07d1a17..699637d 100644 --- a/ui_freenove_allinone/include/app/app_registry.h +++ b/ui_freenove_allinone/include/app/app_registry.h @@ -1,25 +1,46 @@ -// app_registry.h - app catalog loaded from LittleFS with embedded fallback. +// app_registry.h - App catalog loaded from registry.json. #pragma once -#include +#include -#include "app/app_runtime_types.h" +struct AppEntry { + char id[24] = {0}; + char title[32] = {0}; + char category[16] = {0}; + char icon_path[48] = {0}; + char entry_screen[40] = {0}; + bool enabled = true; + uint8_t required_capabilities = 0U; // bitmask (lightweight) +}; -class StorageManager; +// Capability bits for required_capabilities bitmask. +namespace AppCap { +constexpr uint8_t kAudioOut = 0x01U; +constexpr uint8_t kAudioIn = 0x02U; +constexpr uint8_t kStorageFs = 0x04U; +constexpr uint8_t kStorageSd = 0x08U; +constexpr uint8_t kCamera = 0x10U; +constexpr uint8_t kWifi = 0x20U; +constexpr uint8_t kGpuUi = 0x40U; +} // namespace AppCap class AppRegistry { public: - bool loadFromFs(const StorageManager& storage, const char* registry_path = "/apps/registry.json"); - const AppDescriptor* find(const char* id) const; - std::vector listByCategory(const char* category) const; - const std::vector& descriptors() const; + static constexpr uint8_t kMaxApps = 24U; + + AppRegistry() = default; + + bool loadFromJson(const char* json_path); + bool loadFromBuffer(const char* json_buffer, size_t len); + + uint8_t count() const { return count_; } + const AppEntry* entry(uint8_t index) const; + const AppEntry* findById(const char* id) const; + + uint8_t enabledCount() const; + const AppEntry* enabledEntry(uint8_t visible_index) const; private: - bool loadFromJson(const char* json_text); - void loadFallbackCatalog(); - static uint32_t parseCapabilityMask(const char* csv_caps); - static void copyText(char* out, size_t out_size, const char* text); - - std::vector descriptors_; + AppEntry entries_[kMaxApps]; + uint8_t count_ = 0U; }; - diff --git a/ui_freenove_allinone/include/app/app_runtime_manager.h b/ui_freenove_allinone/include/app/app_runtime_manager.h index 4a27335..686645b 100644 --- a/ui_freenove_allinone/include/app/app_runtime_manager.h +++ b/ui_freenove_allinone/include/app/app_runtime_manager.h @@ -1,31 +1,67 @@ -// app_runtime_manager.h - app lifecycle and capability gate. +// app_runtime_manager.h - App lifecycle: open/close/action state machine. #pragma once -#include - +#include +#include "app/app_module.h" #include "app/app_registry.h" -#include "app/app_runtime_types.h" + +struct RuntimeServices; class AppRuntimeManager { public: + enum class State : uint8_t { + kIdle = 0, + kStarting, + kRunning, + kClosing, + kError, + }; + + struct Snapshot { + State state = State::kIdle; + const char* app_id = ""; + const char* entry_screen = ""; + const char* last_error = ""; + uint32_t opened_at_ms = 0U; + }; + + static constexpr uint8_t kMaxModules = 24U; + AppRuntimeManager() = default; - void configure(AppRegistry* registry, const AppContext& context); - bool startApp(const AppStartRequest& request, uint32_t now_ms); - bool stopApp(const AppStopRequest& request, uint32_t now_ms); - bool handleAction(const AppAction& action, uint32_t now_ms); + void begin(AppRegistry* registry, RuntimeServices* services); + + // Register a module (call during setup, before any open). + bool registerModule(IAppModule* module); + + // App lifecycle. + bool open(const char* app_id, const char* mode, uint32_t now_ms); + bool close(const char* reason, uint32_t now_ms); + bool action(const char* action_name, const char* payload); void tick(uint32_t now_ms); - AppRuntimeStatus current() const; - const AppDescriptor* currentDescriptor() const; + // Status. + State state() const { return state_; } + Snapshot snapshot() const; + bool isRunning() const { return state_ == State::kRunning; } + bool isIdle() const { return state_ == State::kIdle; } + const char* currentAppId() const { return current_app_id_; } private: - uint32_t evaluateMissingCapabilities(const AppDescriptor& descriptor) const; - static void copyText(char* out, size_t out_size, const char* text); + IAppModule* findModule(const char* app_id) const; + void transitionTo(State next, const char* reason); AppRegistry* registry_ = nullptr; - AppContext context_ = {}; - std::unique_ptr module_; - const AppDescriptor* current_descriptor_ = nullptr; - AppRuntimeStatus status_ = {}; + RuntimeServices* services_ = nullptr; + + IAppModule* modules_[kMaxModules] = {nullptr}; + uint8_t module_count_ = 0U; + + IAppModule* active_module_ = nullptr; + State state_ = State::kIdle; + char current_app_id_[24] = {0}; + char current_entry_screen_[40] = {0}; + char last_error_[64] = {0}; + uint32_t opened_at_ms_ = 0U; + uint32_t close_requested_ms_ = 0U; }; diff --git a/ui_freenove_allinone/include/app/default_app_registry.h b/ui_freenove_allinone/include/app/default_app_registry.h new file mode 100644 index 0000000..6ff3460 --- /dev/null +++ b/ui_freenove_allinone/include/app/default_app_registry.h @@ -0,0 +1,27 @@ +// default_app_registry.h - Embedded fallback app registry JSON. +#pragma once + +static const char kDefaultAppRegistryJson[] = R"JSON({ + "apps": [ + {"id": "audio_player", "title": "Lecteur Audio", "category": "media", "entry_screen": "SCENE_AUDIO_PLAYER", "icon_path": "/apps/icons/audio.bin", "enabled": true, "required_capabilities": ["CAP_AUDIO_OUT","CAP_STORAGE_FS","CAP_GPU_UI"]}, + {"id": "camera_video", "title": "Photo / Video", "category": "capture", "entry_screen": "SCENE_PHOTO_MANAGER", "icon_path": "/apps/icons/camera.bin", "enabled": true, "required_capabilities": ["CAP_CAMERA","CAP_STORAGE_FS","CAP_GPU_UI"]}, + {"id": "dictaphone", "title": "Dictaphone", "category": "capture", "entry_screen": "SCENE_RECORDER", "icon_path": "/apps/icons/mic.bin", "enabled": true, "required_capabilities": ["CAP_AUDIO_IN","CAP_STORAGE_FS","CAP_GPU_UI"]}, + {"id": "qr_scanner", "title": "QR Scanner", "category": "utility", "entry_screen": "SCENE_QR_DETECTOR", "icon_path": "/apps/icons/qr.bin", "enabled": true, "required_capabilities": ["CAP_CAMERA","CAP_GPU_UI"]}, + {"id": "calculator", "title": "Calculatrice", "category": "utility", "entry_screen": "SCENE_CALCULATOR", "icon_path": "/apps/icons/calc.bin", "enabled": true, "required_capabilities": ["CAP_GPU_UI"]}, + {"id": "timer_tools", "title": "Chrono / Timer", "category": "utility", "entry_screen": "SCENE_TIMER", "icon_path": "/apps/icons/timer.bin", "enabled": true, "required_capabilities": ["CAP_GPU_UI"]}, + {"id": "flashlight", "title": "Lampe", "category": "utility", "entry_screen": "SCENE_FLASHLIGHT", "icon_path": "/apps/icons/flash.bin", "enabled": true, "required_capabilities": ["CAP_GPU_UI"]}, + {"id": "audiobook_player", "title": "Livres Audio", "category": "media", "entry_screen": "SCENE_AUDIOBOOK", "icon_path": "/apps/icons/book.bin", "enabled": true, "required_capabilities": ["CAP_AUDIO_OUT","CAP_STORAGE_FS","CAP_GPU_UI"]}, + {"id": "kids_webradio", "title": "Webradio", "category": "kids", "entry_screen": "SCENE_WEBRADIO", "icon_path": "/apps/icons/radio.bin", "enabled": true, "required_capabilities": ["CAP_AUDIO_OUT","CAP_WIFI","CAP_GPU_UI"]}, + {"id": "kids_podcast", "title": "Podcasts", "category": "kids", "entry_screen": "SCENE_PODCAST", "icon_path": "/apps/icons/podcast.bin", "enabled": true, "required_capabilities": ["CAP_AUDIO_OUT","CAP_WIFI","CAP_GPU_UI"]}, + {"id": "kids_music", "title": "Musique", "category": "kids", "entry_screen": "SCENE_KIDS_MUSIC", "icon_path": "/apps/icons/music.bin", "enabled": true, "required_capabilities": ["CAP_AUDIO_OUT","CAP_STORAGE_FS","CAP_GPU_UI"]}, + {"id": "kids_yoga", "title": "Yoga", "category": "kids", "entry_screen": "SCENE_KIDS_YOGA", "icon_path": "/apps/icons/yoga.bin", "enabled": true, "required_capabilities": ["CAP_AUDIO_OUT","CAP_GPU_UI"]}, + {"id": "kids_meditation", "title": "Meditation", "category": "kids", "entry_screen": "SCENE_KIDS_MEDITATION","icon_path": "/apps/icons/meditate.bin","enabled": true, "required_capabilities": ["CAP_AUDIO_OUT","CAP_GPU_UI"]}, + {"id": "kids_languages", "title": "Langues", "category": "kids", "entry_screen": "SCENE_KIDS_LANG", "icon_path": "/apps/icons/lang.bin", "enabled": true, "required_capabilities": ["CAP_AUDIO_OUT","CAP_GPU_UI"]}, + {"id": "kids_math", "title": "Maths", "category": "kids", "entry_screen": "SCENE_KIDS_MATH", "icon_path": "/apps/icons/math.bin", "enabled": true, "required_capabilities": ["CAP_GPU_UI"]}, + {"id": "kids_science", "title": "Sciences", "category": "kids", "entry_screen": "SCENE_KIDS_SCIENCE", "icon_path": "/apps/icons/science.bin", "enabled": true, "required_capabilities": ["CAP_GPU_UI"]}, + {"id": "kids_geography", "title": "Geographie", "category": "kids", "entry_screen": "SCENE_KIDS_GEO", "icon_path": "/apps/icons/geo.bin", "enabled": true, "required_capabilities": ["CAP_GPU_UI"]}, + {"id": "kids_drawing", "title": "Dessin", "category": "kids", "entry_screen": "SCENE_DRAWING", "icon_path": "/apps/icons/draw.bin", "enabled": true, "required_capabilities": ["CAP_GPU_UI"]}, + {"id": "kids_coloring", "title": "Coloriage", "category": "kids", "entry_screen": "SCENE_COLORING", "icon_path": "/apps/icons/color.bin", "enabled": true, "required_capabilities": ["CAP_GPU_UI"]}, + {"id": "nes_emulator", "title": "NES Emu", "category": "emulator", "entry_screen": "SCENE_NES_EMU", "icon_path": "/apps/icons/nes.bin", "enabled": true, "required_capabilities": ["CAP_GPU_UI","CAP_STORAGE_FS"]} + ] +})JSON"; diff --git a/ui_freenove_allinone/include/app/file_share_service.h b/ui_freenove_allinone/include/app/file_share_service.h index f5ffe02..1a9a0e2 100644 --- a/ui_freenove_allinone/include/app/file_share_service.h +++ b/ui_freenove_allinone/include/app/file_share_service.h @@ -34,7 +34,6 @@ class FileShareService { String* out_error) const; private: - static void copyText(char* out, size_t out_size, const char* text); bool ensureSharedDirs() const; bool resolveIncomingPath(const char* requested_path, String* out_full_path) const; diff --git a/ui_freenove_allinone/include/app/modules/audio_player_module.h b/ui_freenove_allinone/include/app/modules/audio_player_module.h new file mode 100644 index 0000000..2cf210c --- /dev/null +++ b/ui_freenove_allinone/include/app/modules/audio_player_module.h @@ -0,0 +1,20 @@ +// audio_player_module.h - Audio player app module wrapping AmigaAudioPlayer. +#pragma once + +#include "app/app_module.h" + +class AudioPlayerModule : public IAppModule { + public: + const char* id() const override { return "audio_player"; } + + bool onOpen(const AppEntry& entry, const char* mode, RuntimeServices* services) override; + void onClose(const char* reason, RuntimeServices* services) override; + void onTick(uint32_t now_ms, RuntimeServices* services) override; + bool onAction(const char* action, const char* payload, RuntimeServices* services) override; + AppModuleState state() const override { return state_; } + const char* lastError() const override { return last_error_; } + + private: + AppModuleState state_ = AppModuleState::kIdle; + char last_error_[48] = {0}; +}; diff --git a/ui_freenove_allinone/include/audio/audio_manager.h b/ui_freenove_allinone/include/audio/audio_manager.h index 4337b78..5a740a9 100644 --- a/ui_freenove_allinone/include/audio/audio_manager.h +++ b/ui_freenove_allinone/include/audio/audio_manager.h @@ -27,6 +27,8 @@ class AudioManager { void stop(); void update(); bool isPlaying() const; + void releaseOutputResources(); + bool restoreOutputResources(); void setVolume(uint8_t volume); uint8_t volume() const; @@ -112,6 +114,8 @@ class AudioManager { bool pending_use_sd_ = false; bool pending_diagnostic_tone_ = false; uint32_t reopen_earliest_ms_ = 0U; + bool restore_pending_ = false; + uint32_t restore_retry_not_before_ms_ = 0U; uint32_t underrun_count_ = 0U; uint32_t underrun_last_note_ms_ = 0U; uint32_t underrun_last_log_ms_ = 0U; diff --git a/ui_freenove_allinone/include/core/mutex_manager.h b/ui_freenove_allinone/include/core/mutex_manager.h index 622de47..c17b52d 100644 --- a/ui_freenove_allinone/include/core/mutex_manager.h +++ b/ui_freenove_allinone/include/core/mutex_manager.h @@ -1,63 +1,108 @@ -// mutex_manager.h - Thread-safe access protection for global AudioManager and ScenarioManager -// CRITICAL: Prevents race conditions between Arduino loop (core 1), WebServer (core 0), I2S callbacks +// mutex_manager.h - Thread-safe mutex management (OO singleton, atomic stats). +// Strategy: 2 separate mutexes (audio, scenario) for fine-grained protection. +// Lock ordering: ALWAYS audio → scenario to prevent deadlock. +// Overhead: ~50µs per lock/unlock on ESP32-S3 @ 240MHz. #pragma once +#include +#include + #include #include #include // ============================================================================ -// MUTEX MANAGER - Dual-mutex strategy for g_audio and g_scenario protection -// Strategy: 2 separate mutexes (audio_mutex, scenario_mutex) for fine granularity -// Lock ordering: ALWAYS acquire audio_mutex before scenario_mutex to prevent deadlock -// ISR-safe: Timeout-based acquisition (max 1000ms) compatible with watchdog (30s) -// Overhead: ~50µs per lock/unlock operation on ESP32-S3 @ 240MHz +// MutexManager — singleton class with atomic stats +// Call sites unchanged: MutexManager::init(), MutexManager::takeAudioMutex()… // ============================================================================ -namespace MutexManager { +class MutexManager { + public: + // Meyer's singleton — thread-safe, zero-overhead after first call. + static MutexManager& instance(); -// Initialize mutex system (call once in setup()) -// Returns true if initialization successful, false on failure -bool init(); + // --- Lifecycle (call once in setup / shutdown) --- + bool doInit(); + void doDeinit(); -// Cleanup (call on shutdown/restart) -void deinit(); + // --- Low-level acquisition (prefer RAII guards below) --- + bool takeAudio(uint32_t timeout_ms); + void releaseAudio(); + bool takeScenario(uint32_t timeout_ms); + void releaseScenario(); + bool takeBoth(uint32_t timeout_ms); + void releaseBoth(); -// Low-level mutex acquisition (prefer RAII guards below) -// timeout_ms: Max wait time (0 = no wait, portMAX_DELAY = infinite) -// Returns true if lock acquired, false on timeout -bool takeAudioMutex(uint32_t timeout_ms); -void releaseAudioMutex(); + // --- Atomic stats --- + uint32_t audioLockCount() const { return audio_lock_count_.load(); } + uint32_t scenarioLockCount() const { return scenario_lock_count_.load(); } + uint32_t audioTimeoutCount() const { return audio_timeout_count_.load(); } + uint32_t scenarioTimeoutCount() const { return scenario_timeout_count_.load(); } + uint32_t maxAudioWaitUs() const { return max_audio_wait_us_.load(); } + uint32_t maxScenarioWaitUs() const { return max_scenario_wait_us_.load(); } -bool takeScenarioMutex(uint32_t timeout_ms); -void releaseScenarioMutex(); + // --- Static wrappers — backward-compatible with old namespace API --- + static bool init() { return instance().doInit(); } + static void deinit() { instance().doDeinit(); } + static bool takeAudioMutex(uint32_t ms) { return instance().takeAudio(ms); } + static void releaseAudioMutex() { instance().releaseAudio(); } + static bool takeScenarioMutex(uint32_t ms) { return instance().takeScenario(ms); } + static void releaseScenarioMutex() { instance().releaseScenario(); } + static bool takeBothMutexes(uint32_t ms) { return instance().takeBoth(ms); } + static void releaseBothMutexes() { instance().releaseBoth(); } + static uint32_t getAudioLockCount() { return instance().audioLockCount(); } + static uint32_t getScenarioLockCount() { return instance().scenarioLockCount(); } + static uint32_t getAudioTimeoutCount() { return instance().audioTimeoutCount(); } + static uint32_t getScenarioTimeoutCount() { return instance().scenarioTimeoutCount(); } + static uint32_t getMaxAudioWaitUs() { return instance().maxAudioWaitUs(); } + static uint32_t getMaxScenarioWaitUs() { return instance().maxScenarioWaitUs(); } -// Acquire both mutexes with deadlock prevention (audio → scenario order) -// Returns true if both acquired, false if either acquisition failed -bool takeBothMutexes(uint32_t timeout_ms); -void releaseBothMutexes(); + // Non-copyable, non-movable. + MutexManager(const MutexManager&) = delete; + MutexManager& operator=(const MutexManager&) = delete; -// Statistics (for debugging race conditions) -uint32_t audioLockCount(); -uint32_t scenarioLockCount(); -uint32_t audioTimeoutCount(); -uint32_t scenarioTimeoutCount(); -uint32_t maxAudioWaitUs(); -uint32_t maxScenarioWaitUs(); + private: + MutexManager() = default; -} // namespace MutexManager + void updateMaxWait(std::atomic& max_field, uint32_t elapsed_us); + + SemaphoreHandle_t audio_mutex_{nullptr}; + SemaphoreHandle_t scenario_mutex_{nullptr}; + + std::atomic audio_lock_count_{0U}; + std::atomic scenario_lock_count_{0U}; + std::atomic audio_timeout_count_{0U}; + std::atomic scenario_timeout_count_{0U}; + std::atomic max_audio_wait_us_{0U}; + std::atomic max_scenario_wait_us_{0U}; + + // Owner tracking for deadlock detection (volatile: written under semaphore). + volatile TaskHandle_t audio_owner_{nullptr}; + volatile TaskHandle_t scenario_owner_{nullptr}; +}; // ============================================================================ -// RAII LOCK GUARDS - Automatic lock/unlock with timeout protection -// Usage: { AudioLock lock; g_audio.play(...); } // auto-release on scope exit +// RAII LOCK GUARDS — automatic lock/unlock on scope exit. +// Usage: { AudioLock lock; if (lock) g_audio.play(…); } // ============================================================================ class AudioLock { public: - explicit AudioLock(uint32_t timeout_ms = 1000); - ~AudioLock(); + explicit AudioLock(uint32_t timeout_ms = 1000U) + : acquired_(MutexManager::instance().takeAudio(timeout_ms)) { + if (!acquired_) { + Serial.printf("[MUTEX] AudioLock FAILED (timeout %lu ms)\n", + static_cast(timeout_ms)); + } + } + ~AudioLock() { + if (acquired_) { + MutexManager::instance().releaseAudio(); + } + } AudioLock(const AudioLock&) = delete; AudioLock& operator=(const AudioLock&) = delete; + explicit operator bool() const { return acquired_; } bool acquired() const { return acquired_; } private: @@ -66,10 +111,21 @@ class AudioLock { class ScenarioLock { public: - explicit ScenarioLock(uint32_t timeout_ms = 1000); - ~ScenarioLock(); + explicit ScenarioLock(uint32_t timeout_ms = 1000U) + : acquired_(MutexManager::instance().takeScenario(timeout_ms)) { + if (!acquired_) { + Serial.printf("[MUTEX] ScenarioLock FAILED (timeout %lu ms)\n", + static_cast(timeout_ms)); + } + } + ~ScenarioLock() { + if (acquired_) { + MutexManager::instance().releaseScenario(); + } + } ScenarioLock(const ScenarioLock&) = delete; ScenarioLock& operator=(const ScenarioLock&) = delete; + explicit operator bool() const { return acquired_; } bool acquired() const { return acquired_; } private: @@ -78,10 +134,21 @@ class ScenarioLock { class DualLock { public: - explicit DualLock(uint32_t timeout_ms = 1000); - ~DualLock(); + explicit DualLock(uint32_t timeout_ms = 1000U) + : acquired_(MutexManager::instance().takeBoth(timeout_ms)) { + if (!acquired_) { + Serial.printf("[MUTEX] DualLock FAILED (timeout %lu ms)\n", + static_cast(timeout_ms)); + } + } + ~DualLock() { + if (acquired_) { + MutexManager::instance().releaseBoth(); + } + } DualLock(const DualLock&) = delete; DualLock& operator=(const DualLock&) = delete; + explicit operator bool() const { return acquired_; } bool acquired() const { return acquired_; } private: @@ -89,14 +156,13 @@ class DualLock { }; // ============================================================================ -// INTEGRATION MACROS - Quick integration into existing code -// Example: AUDIO_GUARDED_CALL(g_audio.play("/music/track.mp3")) +// MACROS — quick guarded calls (unchanged API). // ============================================================================ #define AUDIO_GUARDED_CALL(call) \ do { \ - AudioLock _audio_lock(1000); \ - if (!_audio_lock.acquired()) { \ + AudioLock _audio_lock(1000U); \ + if (!_audio_lock) { \ Serial.println("[MUTEX] WARN: Audio lock timeout"); \ } else { \ call; \ @@ -105,8 +171,8 @@ class DualLock { #define SCENARIO_GUARDED_CALL(call) \ do { \ - ScenarioLock _scenario_lock(1000); \ - if (!_scenario_lock.acquired()) { \ + ScenarioLock _scenario_lock(1000U); \ + if (!_scenario_lock) { \ Serial.println("[MUTEX] WARN: Scenario lock timeout"); \ } else { \ call; \ @@ -115,8 +181,8 @@ class DualLock { #define DUAL_GUARDED_CALL(call) \ do { \ - DualLock _dual_lock(1000); \ - if (!_dual_lock.acquired()) { \ + DualLock _dual_lock(1000U); \ + if (!_dual_lock) { \ Serial.println("[MUTEX] WARN: Dual lock timeout"); \ } else { \ call; \ diff --git a/ui_freenove_allinone/include/core/str_utils.h b/ui_freenove_allinone/include/core/str_utils.h new file mode 100644 index 0000000..8929945 --- /dev/null +++ b/ui_freenove_allinone/include/core/str_utils.h @@ -0,0 +1,46 @@ +// str_utils.h - shared string utilities (zero-dependency, header-only). +#pragma once + +#include +#include +#include + +namespace core { + +/// Case-insensitive string comparison. Returns true if lhs == rhs ignoring case. +/// Safe with nullptr: (nullptr, nullptr) → false. +inline bool equalsIgnoreCase(const char* lhs, const char* rhs) { + if (lhs == nullptr || rhs == nullptr) { + return false; + } + for (size_t i = 0U;; ++i) { + const char a = lhs[i]; + const char b = rhs[i]; + if (a == '\0' && b == '\0') { + return true; + } + if (a == '\0' || b == '\0') { + return false; + } + if (std::tolower(static_cast(a)) != + std::tolower(static_cast(b))) { + return false; + } + } +} + +/// Safe bounded string copy. Always null-terminates dst. +/// No-op if dst is nullptr or dst_size is 0. +inline void copyText(char* dst, size_t dst_size, const char* src) { + if (dst == nullptr || dst_size == 0U) { + return; + } + if (src == nullptr) { + dst[0] = '\0'; + return; + } + std::strncpy(dst, src, dst_size - 1U); + dst[dst_size - 1U] = '\0'; +} + +} // namespace core diff --git a/ui_freenove_allinone/include/drivers/board/hardware_manager.h b/ui_freenove_allinone/include/drivers/board/hardware_manager.h index 46dd8f2..ab53711 100644 --- a/ui_freenove_allinone/include/drivers/board/hardware_manager.h +++ b/ui_freenove_allinone/include/drivers/board/hardware_manager.h @@ -89,6 +89,7 @@ class HardwareManager { private: bool beginMic(); + void endMic(); void updateMic(uint32_t now_ms); void updateBattery(uint32_t now_ms); void updateLed(uint32_t now_ms); diff --git a/ui_freenove_allinone/include/runtime/perf/perf_monitor.h b/ui_freenove_allinone/include/runtime/perf/perf_monitor.h index 7331f27..fdf9fb0 100644 --- a/ui_freenove_allinone/include/runtime/perf/perf_monitor.h +++ b/ui_freenove_allinone/include/runtime/perf/perf_monitor.h @@ -2,6 +2,7 @@ #pragma once #include +#include #include @@ -48,6 +49,8 @@ class PerfMonitor { PerfSectionStats sections_[static_cast(PerfSection::kCount)] = {}; uint32_t ui_dma_flush_count_ = 0U; uint32_t ui_sync_flush_count_ = 0U; + // Spinlock protecting sections_ and flush counters (multi-task safe). + mutable portMUX_TYPE stats_mux_ = portMUX_INITIALIZER_UNLOCKED; }; PerfMonitor& perfMonitor(); diff --git a/ui_freenove_allinone/include/runtime/simd/simd_accel.h b/ui_freenove_allinone/include/runtime/simd/simd_accel.h index 4a5da80..a13ae5c 100644 --- a/ui_freenove_allinone/include/runtime/simd/simd_accel.h +++ b/ui_freenove_allinone/include/runtime/simd/simd_accel.h @@ -32,19 +32,52 @@ struct SimdBenchResult { const SimdAccelStatus& status(); void resetBenchStatus(); +// --- RGB565 buffer operations --- + +/// memcpy wrapper for typed RGB565 buffers. void simd_rgb565_copy(uint16_t* dst, const uint16_t* src, size_t n_px); + +/// Fill n_px pixels with a constant RGB565 color. Uses 32-bit writes when aligned. void simd_rgb565_fill(uint16_t* dst, uint16_t color565, size_t n_px); + +/// Byte-swap every RGB565 pixel (host ↔ big-endian wire order). +/// Optimised: 2 pixels per iteration via 32-bit packing when aligned. void simd_rgb565_bswap_copy(uint16_t* dst, const uint16_t* src, size_t n_px); +/// Alpha-blend src over dst in-place. +/// alpha = 255 → fully src, alpha = 0 → fully dst. +/// Optimised: 2 pixels per iteration via 32-bit masking when aligned. +void simd_rgb565_alpha_blend(uint16_t* dst, const uint16_t* src, + uint8_t alpha, size_t n_px); + +/// Scale brightness of each pixel. level = 255 → no change, 0 → black. +/// Optimised: 2 pixels per iteration when aligned. +void simd_rgb565_brightness(uint16_t* dst, const uint16_t* src, + uint8_t level, size_t n_px); + +// --- Color format conversions --- + +/// Grayscale L8 → RGB565 via precomputed LUT. 2-pixel 32-bit fast path when aligned. void simd_l8_to_rgb565(uint16_t* dst565, const uint8_t* src_l8, size_t n_px); + +/// Indexed 8-bit palette → RGB565. 2-pixel 32-bit fast path when aligned. void simd_index8_to_rgb565(uint16_t* dst565, const uint8_t* idx8, const uint16_t* pal565_256, size_t n_px); + +/// Packed RGB888 → RGB565. 32-bit read fast path when src is aligned. void simd_rgb888_to_rgb565(uint16_t* dst565, const uint8_t* src_rgb888, size_t n_px); + +/// YUV422 (YUYV) → RGB565. BT.601 full-range, 2 pixels per iteration. void simd_yuv422_to_rgb565(uint16_t* dst565, const uint8_t* src_yuv422, size_t n_px); +// --- Audio DSP --- + +/// Apply Q15 gain to int16 samples. Uses ESP-DSP when available, scalar fallback. void simd_s16_gain_q15(int16_t* dst, const int16_t* src, int16_t gain_q15, size_t n); + +/// Mix two int16 streams with independent Q15 gains. Uses ESP-DSP when available. void simd_s16_mix2_q15(int16_t* dst, const int16_t* a, const int16_t* b, @@ -52,8 +85,9 @@ void simd_s16_mix2_q15(int16_t* dst, int16_t gb_q15, size_t n); +// --- Self-test and benchmark --- + bool selfTest(); SimdBenchResult runBench(uint32_t loops, uint32_t pixels); } // namespace runtime::simd - diff --git a/ui_freenove_allinone/include/system/network/network_manager.h b/ui_freenove_allinone/include/system/network/network_manager.h index 32bc6e3..d183ce6 100644 --- a/ui_freenove_allinone/include/system/network/network_manager.h +++ b/ui_freenove_allinone/include/system/network/network_manager.h @@ -88,9 +88,7 @@ class NetworkManager { static void onEspNowSend(const uint8_t* mac_addr, esp_now_send_status_t status); static uint8_t parseHexByte(char high, char low, bool* ok); - static void copyText(char* out, size_t out_size, const char* text); static void formatMac(const uint8_t* mac, char* out, size_t out_size); - static bool equalsIgnoreCase(const char* lhs, const char* rhs); static const char* wifiModeLabel(uint8_t mode); static const char* networkStateLabel(bool sta_connected, bool sta_connecting, diff --git a/ui_freenove_allinone/include/ui/workbench/amiga_ui_shell.h b/ui_freenove_allinone/include/ui/workbench/amiga_ui_shell.h new file mode 100644 index 0000000..54ef962 --- /dev/null +++ b/ui_freenove_allinone/include/ui/workbench/amiga_ui_shell.h @@ -0,0 +1,131 @@ +// amiga_ui_shell.h - Workbench-style app launcher grid (LVGL). +#pragma once + +#include +#include + +#include "app/app_registry.h" +#include "app/app_runtime_manager.h" + +namespace ui::workbench { + +class AmigaUIShell { + public: + // Display layout: portrait 320x480 => 3 cols x 5 rows = 15 tiles/page. + // Landscape 480x320 => 4 cols x 3 rows = 12 tiles/page. + static constexpr uint8_t kGridColsPortrait = 3U; + static constexpr uint8_t kGridRowsPortrait = 5U; + static constexpr uint8_t kGridColsLandscape = 4U; + static constexpr uint8_t kGridRowsLandscape = 3U; + + static constexpr uint8_t kTilesPerPage = 12U; // Conservative: fits both orientations. + static constexpr uint8_t kMaxPages = 3U; + static constexpr uint8_t kMaxVisibleTiles = kTilesPerPage * kMaxPages; + + enum class InputKey : uint8_t { + kUp = 0, + kDown, + kLeft, + kRight, + kOk, + kBack, + }; + + struct Config { + bool landscape = true; // Default: rotation=1 => landscape. + uint16_t screen_w = 480U; + uint16_t screen_h = 320U; + uint8_t grid_cols = kGridColsLandscape; + uint8_t grid_rows = kGridRowsLandscape; + }; + + AmigaUIShell() = default; + ~AmigaUIShell() = default; + AmigaUIShell(const AmigaUIShell&) = delete; + AmigaUIShell& operator=(const AmigaUIShell&) = delete; + + bool begin(const Config& cfg, AppRegistry* registry, AppRuntimeManager* runtime); + void show(); + void hide(); + bool visible() const { return visible_; } + void toggle(); + + void handleInput(InputKey key); + void handleButtonKey(uint8_t hw_key, bool long_press); + void tick(uint32_t now_ms); + + // Request app launch from the currently focused tile. + bool launchFocused(uint32_t now_ms); + bool requestCloseApp(const char* reason, uint32_t now_ms); + + uint8_t currentPage() const { return current_page_; } + uint8_t pageCount() const { return page_count_; } + uint8_t focusIndex() const { return focus_index_; } + const AppEntry* focusedEntry() const; + + private: + void createUi(); + void destroyUi(); + void rebuildGrid(); + void updateFocusVisual(); + void updateStatusBar(uint32_t now_ms); + void updateTileStates(); + void navigatePage(int8_t delta); + void navigateFocus(int8_t dx, int8_t dy); + uint8_t tilesOnCurrentPage() const; + uint8_t globalIndexFromPageLocal(uint8_t local) const; + + // Styles. + lv_style_t st_bg_; + lv_style_t st_topbar_; + lv_style_t st_tile_; + lv_style_t st_tile_focused_; + lv_style_t st_tile_disabled_; + lv_style_t st_label_; + lv_style_t st_status_; + lv_style_t st_page_indicator_; + bool styles_inited_ = false; + + // Layout. + Config cfg_; + uint16_t tile_w_ = 0U; + uint16_t tile_h_ = 0U; + uint16_t grid_x_offset_ = 0U; + uint16_t grid_y_offset_ = 0U; + + // Data. + AppRegistry* registry_ = nullptr; + AppRuntimeManager* runtime_ = nullptr; + uint8_t enabled_count_ = 0U; + uint8_t page_count_ = 0U; + uint8_t current_page_ = 0U; + uint8_t focus_index_ = 0U; // local index on current page + bool visible_ = false; + + // LVGL objects. + lv_obj_t* root_ = nullptr; + lv_obj_t* topbar_ = nullptr; + lv_obj_t* topbar_label_ = nullptr; + lv_obj_t* topbar_status_ = nullptr; + lv_obj_t* grid_container_ = nullptr; + lv_obj_t* page_indicator_ = nullptr; + + static constexpr uint8_t kMaxTilesPerPage = 15U; // Max visible on one page. + struct TileSlot { + lv_obj_t* container = nullptr; + lv_obj_t* icon_label = nullptr; + lv_obj_t* title_label = nullptr; + uint8_t global_app_index = 0xFFU; + }; + TileSlot tiles_[kMaxTilesPerPage]; + uint8_t tile_count_ = 0U; + + // Launching feedback. + lv_obj_t* launch_overlay_ = nullptr; + lv_obj_t* launch_label_ = nullptr; + uint32_t launch_overlay_hide_ms_ = 0U; + + uint32_t last_status_update_ms_ = 0U; +}; + +} // namespace ui::workbench diff --git a/ui_freenove_allinone/include/ui_freenove_config.h b/ui_freenove_allinone/include/ui_freenove_config.h index eae1d16..6c77829 100644 --- a/ui_freenove_allinone/include/ui_freenove_config.h +++ b/ui_freenove_allinone/include/ui_freenove_config.h @@ -1,8 +1,9 @@ #pragma once -// Freenove Media Kit reference profile (Sketch_19): -// - FNK0102H ST7796 320x480 by default, rotation 1 -// - Buttons via analog ladder on GPIO19 +// Freenove FNK0102H release profile: +// - Freenove ESP32-S3 WROOM N8R8 (8MB flash / 8MB PSRAM) +// - ST7796 320x480, rotation 1 +// - 5-way switch via analog ladder on GPIO19 // - I2S output BCLK=42, WS=41, DOUT=1 #ifndef FREENOVE_LCD_VARIANT_FNK0102A @@ -135,6 +136,9 @@ #define FREENOVE_HAS_TOUCH 0 #endif +// Official release profile uses the 5-way switch only. +// The experimental touch wiring overlaps with the camera mapping on FNK0102H, +// so release builds keep touch disabled. #if FREENOVE_HAS_TOUCH #define FREENOVE_TOUCH_CS 9 #define FREENOVE_TOUCH_IRQ 15 @@ -221,7 +225,7 @@ #define FREENOVE_BAT_CHARGE_PIN -1 #endif -// Camera wiring from Freenove FNK0102 (ESP32S3_EYE profile). +// Camera wiring validated for the FNK0102H release profile. #ifndef FREENOVE_CAM_ENABLE #define FREENOVE_CAM_ENABLE 1 #endif diff --git a/ui_freenove_allinone/src/app/app_audio.cpp b/ui_freenove_allinone/src/app/app_audio.cpp index b6a5480..07a49e8 100644 --- a/ui_freenove_allinone/src/app/app_audio.cpp +++ b/ui_freenove_allinone/src/app/app_audio.cpp @@ -1,6 +1,7 @@ // app_audio.cpp - Audio Player Implementation #include "app/app_audio.h" #include "audio_manager.h" +#include "core/str_utils.h" AppAudio g_app_audio; @@ -54,7 +55,7 @@ void AppAudio::onAction(const AppAction& action) { } else if (strcmp(action.name, "stop") == 0) { stop(); } else if (strcmp(action.name, "volume") == 0) { - int vol = atoi(action.payload); + int vol = static_cast(strtol(action.payload, nullptr, 10)); setVolume(constrain(vol, 0, 100)); } } @@ -65,8 +66,7 @@ bool AppAudio::play(const char* filepath) { return false; } - strncpy(current_file_, filepath, sizeof(current_file_) - 1); - current_file_[sizeof(current_file_) - 1] = '\0'; + core::copyText(current_file_, sizeof(current_file_), filepath); playback_ms_ = 0; duration_ms_ = 30000; // Assume 30 seconds for demo @@ -74,9 +74,8 @@ bool AppAudio::play(const char* filepath) { Serial.printf("[APP_AUDIO] Playing: %s (vol=%u%%)\n", current_file_, volume_percent_); - // TODO: Integrate with AudioManager for actual playback if (context_.audio) { - // context_.audio->playFile(filepath); + context_.audio->play(filepath); } return true; @@ -104,9 +103,8 @@ void AppAudio::stop() { Serial.println("[APP_AUDIO] Stopped"); - // TODO: Stop playback via AudioManager if (context_.audio) { - // context_.audio->stop(); + context_.audio->stop(); } } @@ -114,9 +112,8 @@ void AppAudio::setVolume(uint8_t vol_percent) { volume_percent_ = vol_percent; Serial.printf("[APP_AUDIO] Volume: %u%%\n", volume_percent_); - // TODO: Update hardware volume via AudioManager if (context_.audio) { - // context_.audio->setVolume(vol_percent); + context_.audio->setVolume(vol_percent); } } diff --git a/ui_freenove_allinone/src/app/app_calculator.cpp b/ui_freenove_allinone/src/app/app_calculator.cpp index 9895aa4..1d4105a 100644 --- a/ui_freenove_allinone/src/app/app_calculator.cpp +++ b/ui_freenove_allinone/src/app/app_calculator.cpp @@ -1,5 +1,6 @@ // app_calculator.cpp - Calculator Implementation #include "app/app_calculator.h" +#include "core/str_utils.h" AppCalculator g_app_calculator; @@ -39,7 +40,7 @@ void AppCalculator::onAction(const AppAction& action) { void AppCalculator::reset() { result_ = 0.0; - strncpy(display_, "0", sizeof(display_) - 1); + core::copyText(display_, sizeof(display_), "0"); history_[0] = '\0'; } diff --git a/ui_freenove_allinone/src/app/app_flashlight.cpp b/ui_freenove_allinone/src/app/app_flashlight.cpp index d5c2a2e..4176a5d 100644 --- a/ui_freenove_allinone/src/app/app_flashlight.cpp +++ b/ui_freenove_allinone/src/app/app_flashlight.cpp @@ -42,7 +42,7 @@ void AppFlashlight::onAction(const AppAction& action) { } else if (strcmp(action.name, "toggle") == 0) { toggle(); } else if (strcmp(action.name, "intensity") == 0) { - int percent = atoi(action.payload); + int percent = static_cast(strtol(action.payload, nullptr, 10)); setIntensity(constrain(percent, 0, 100)); } } diff --git a/ui_freenove_allinone/src/app/app_registry.cpp b/ui_freenove_allinone/src/app/app_registry.cpp index 22b78e3..e3fd34f 100644 --- a/ui_freenove_allinone/src/app/app_registry.cpp +++ b/ui_freenove_allinone/src/app/app_registry.cpp @@ -2,11 +2,9 @@ #include "app/app_registry.h" #include - -#include #include -#include +#include "core/str_utils.h" #include "storage_manager.h" namespace { @@ -16,100 +14,82 @@ struct FallbackAppEntry { const char* title; const char* category; const char* entry_screen; - bool enabled; - uint32_t required; - uint32_t optional; - bool offline; - bool streaming; + bool enabled; + uint32_t required; + uint32_t optional; + bool offline; + bool streaming; const char* manifest; }; constexpr FallbackAppEntry kFallbackApps[] = { - {"audio_player", "Lecteur Audio", "media", "SCENE_AUDIO_PLAYER", true, + {"audio_player", "Lecteur Audio", "media", "SCENE_AUDIO_PLAYER", true, CAP_AUDIO_OUT | CAP_STORAGE_FS | CAP_GPU_UI, CAP_WIFI, - true, true, "/apps/audio_player/manifest.json"}, - {"camera_video", "Appareil Photo/Video", "capture", "SCENE_PHOTO_MANAGER", true, - CAP_CAMERA | CAP_STORAGE_FS | CAP_GPU_UI, - CAP_STORAGE_SD, true, false, "/apps/camera_video/manifest.json"}, - {"dictaphone", "Dictaphone", "capture", "SCENE_RECORDER", true, - CAP_AUDIO_IN | CAP_STORAGE_FS | CAP_GPU_UI, CAP_STORAGE_SD, + true, true, "/apps/audio_player/manifest.json"}, + {"camera_video", "Appareil Photo/Video", "capture", "SCENE_PHOTO_MANAGER", true, + CAP_CAMERA | CAP_STORAGE_FS | CAP_GPU_UI, CAP_STORAGE_SD, + true, false, "/apps/camera_video/manifest.json"}, + {"dictaphone", "Dictaphone", "capture", "SCENE_RECORDER", true, + CAP_AUDIO_IN | CAP_STORAGE_FS | CAP_GPU_UI, CAP_STORAGE_SD, true, false, "/apps/dictaphone/manifest.json"}, - {"timer_tools", "Chronometre/Minuteur", "utility", "SCENE_TIMER", true, - CAP_GPU_UI, 0U, true, false, - "/apps/timer_tools/manifest.json"}, - {"flashlight", "Lampe de Poche", "utility", "SCENE_FLASHLIGHT", true, - CAP_LED | CAP_GPU_UI, 0U, true, false, - "/apps/flashlight/manifest.json"}, - {"calculator", "Calculatrice", "utility", "SCENE_CALCULATOR", true, - CAP_GPU_UI, 0U, true, false, - "/apps/calculator/manifest.json"}, - {"kids_webradio", "Webradio Enfants", "kids_media", "SCENE_WEBRADIO", false, - CAP_AUDIO_OUT | CAP_GPU_UI, - CAP_WIFI | CAP_STORAGE_FS, true, true, "/apps/kids_webradio/manifest.json"}, - {"kids_podcast", "Podcast Enfants", "kids_media", "SCENE_PODCAST", false, + {"timer_tools", "Chronometre/Minuteur", "utility", "SCENE_TIMER", true, + CAP_GPU_UI, 0U, true, false, "/apps/timer_tools/manifest.json"}, + {"flashlight", "Lampe de Poche", "utility", "SCENE_FLASHLIGHT", true, + CAP_LED | CAP_GPU_UI, 0U, true, false, "/apps/flashlight/manifest.json"}, + {"calculator", "Calculatrice", "utility", "SCENE_CALCULATOR", true, + CAP_GPU_UI, 0U, true, false, "/apps/calculator/manifest.json"}, + {"kids_webradio", "Webradio Enfants", "kids_media", "SCENE_WEBRADIO", false, CAP_AUDIO_OUT | CAP_GPU_UI, CAP_WIFI | CAP_STORAGE_FS, - true, true, "/apps/kids_podcast/manifest.json"}, - {"qr_scanner", "Lecteur QR Code", "capture", "SCENE_QR_DETECTOR", true, - CAP_CAMERA | CAP_GPU_UI, 0U, true, false, - "/apps/qr_scanner/manifest.json"}, - {"nes_emulator", "Emulateur NES", "games", "SCENE_NES_EMU", false, + true, true, "/apps/kids_webradio/manifest.json"}, + {"kids_podcast", "Podcast Enfants", "kids_media", "SCENE_PODCAST", false, + CAP_AUDIO_OUT | CAP_GPU_UI, CAP_WIFI | CAP_STORAGE_FS, + true, true, "/apps/kids_podcast/manifest.json"}, + {"qr_scanner", "Lecteur QR Code", "capture", "SCENE_QR_DETECTOR", true, + CAP_CAMERA | CAP_GPU_UI, 0U, true, false, "/apps/qr_scanner/manifest.json"}, + {"nes_emulator", "Emulateur NES", "games", "SCENE_NES_EMU", false, CAP_GPU_UI | CAP_AUDIO_OUT | CAP_STORAGE_FS, CAP_STORAGE_SD, true, false, "/apps/nes_emulator/manifest.json"}, - {"audiobook_player", "Livres Audio", "media", "SCENE_AUDIOBOOK", true, - CAP_AUDIO_OUT | CAP_GPU_UI | CAP_STORAGE_FS, - CAP_WIFI | CAP_STORAGE_SD, true, true, "/apps/audiobook_player/manifest.json"}, - {"kids_drawing", "Dessin", "kids_learning", "SCENE_DRAWING", false, - CAP_GPU_UI, 0U, true, false, - "/apps/kids_drawing/manifest.json"}, - {"kids_coloring", "Coloriage", "kids_learning", "SCENE_COLORING", false, - CAP_GPU_UI, 0U, true, false, - "/apps/kids_coloring/manifest.json"}, - {"kids_music", "Musique Enfants", "kids_media", "SCENE_KIDS_MUSIC", false, + {"audiobook_player", "Livres Audio", "media", "SCENE_AUDIOBOOK", true, + CAP_AUDIO_OUT | CAP_GPU_UI | CAP_STORAGE_FS, CAP_WIFI | CAP_STORAGE_SD, + true, true, "/apps/audiobook_player/manifest.json"}, + {"kids_drawing", "Dessin", "kids_learning", "SCENE_DRAWING", false, + CAP_GPU_UI, 0U, true, false, "/apps/kids_drawing/manifest.json"}, + {"kids_coloring", "Coloriage", "kids_learning", "SCENE_COLORING", false, + CAP_GPU_UI, 0U, true, false, "/apps/kids_coloring/manifest.json"}, + {"kids_music", "Musique Enfants", "kids_media", "SCENE_KIDS_MUSIC", false, CAP_AUDIO_OUT | CAP_GPU_UI, CAP_WIFI | CAP_STORAGE_FS, - true, true, "/apps/kids_music/manifest.json"}, - {"kids_yoga", "Yoga Enfants", "kids_wellness", "SCENE_KIDS_YOGA", false, + true, true, "/apps/kids_music/manifest.json"}, + {"kids_yoga", "Yoga Enfants", "kids_wellness", "SCENE_KIDS_YOGA", false, CAP_AUDIO_OUT | CAP_GPU_UI, CAP_WIFI | CAP_STORAGE_FS, - true, true, "/apps/kids_yoga/manifest.json"}, - {"kids_meditation", "Meditation Enfants", "kids_wellness", "SCENE_KIDS_MEDITATION", false, - CAP_AUDIO_OUT | CAP_GPU_UI, CAP_WIFI | CAP_STORAGE_FS, true, true, "/apps/kids_meditation/manifest.json"}, - {"kids_languages", "Langues Enfants", "kids_learning", "SCENE_KIDS_LANG", false, - CAP_AUDIO_OUT | CAP_GPU_UI, - CAP_WIFI | CAP_STORAGE_FS, true, true, "/apps/kids_languages/manifest.json"}, - {"kids_math", "Maths Enfants", "kids_learning", "SCENE_KIDS_MATH", false, + true, true, "/apps/kids_yoga/manifest.json"}, + {"kids_meditation", "Meditation Enfants", "kids_wellness", "SCENE_KIDS_MEDITATION", false, + CAP_AUDIO_OUT | CAP_GPU_UI, CAP_WIFI | CAP_STORAGE_FS, + true, true, "/apps/kids_meditation/manifest.json"}, + {"kids_languages", "Langues Enfants", "kids_learning", "SCENE_KIDS_LANG", false, + CAP_AUDIO_OUT | CAP_GPU_UI, CAP_WIFI | CAP_STORAGE_FS, + true, true, "/apps/kids_languages/manifest.json"}, + {"kids_math", "Maths Enfants", "kids_learning", "SCENE_KIDS_MATH", false, CAP_GPU_UI, CAP_AUDIO_OUT | CAP_WIFI | CAP_STORAGE_FS, - true, true, "/apps/kids_math/manifest.json"}, - {"kids_science", "Sciences Enfants", "kids_learning", "SCENE_KIDS_SCIENCE", false, - CAP_GPU_UI, - CAP_AUDIO_OUT | CAP_WIFI | CAP_STORAGE_FS, true, true, "/apps/kids_science/manifest.json"}, - {"kids_geography", "Geographie Enfants", "kids_learning", "SCENE_KIDS_GEO", false, - CAP_GPU_UI, - CAP_AUDIO_OUT | CAP_WIFI | CAP_STORAGE_FS, true, true, "/apps/kids_geography/manifest.json"}, + true, true, "/apps/kids_math/manifest.json"}, + {"kids_science", "Sciences Enfants", "kids_learning", "SCENE_KIDS_SCIENCE", false, + CAP_GPU_UI, CAP_AUDIO_OUT | CAP_WIFI | CAP_STORAGE_FS, + true, true, "/apps/kids_science/manifest.json"}, + {"kids_geography", "Geographie Enfants", "kids_learning", "SCENE_KIDS_GEO", false, + CAP_GPU_UI, CAP_AUDIO_OUT | CAP_WIFI | CAP_STORAGE_FS, + true, true, "/apps/kids_geography/manifest.json"}, }; -bool equalsIgnoreCase(const char* lhs, const char* rhs) { - if (lhs == nullptr || rhs == nullptr) { - return false; - } - for (size_t i = 0U;; ++i) { - const char a = lhs[i]; - const char b = rhs[i]; - if (a == '\0' && b == '\0') { - return true; - } - if (a == '\0' || b == '\0') { - return false; - } - if (std::tolower(static_cast(a)) != std::tolower(static_cast(b))) { - return false; - } - } -} - } // namespace +// ============================================================================ +// Public API +// ============================================================================ + bool AppRegistry::loadFromFs(const StorageManager& storage, const char* registry_path) { descriptors_.clear(); - const char* path = (registry_path != nullptr && registry_path[0] != '\0') ? registry_path : "/apps/registry.json"; + const char* path = (registry_path != nullptr && registry_path[0] != '\0') + ? registry_path + : "/apps/registry.json"; const String payload = storage.loadTextFile(path); if (payload.isEmpty() || !loadFromJson(payload.c_str())) { loadFallbackCatalog(); @@ -123,26 +103,18 @@ bool AppRegistry::loadFromFs(const StorageManager& storage, const char* registry } const AppDescriptor* AppRegistry::find(const char* id) const { - if (id == nullptr || id[0] == '\0') { - return nullptr; - } - for (const AppDescriptor& descriptor : descriptors_) { - if (equalsIgnoreCase(descriptor.id, id)) { - return &descriptor; - } + if (id == nullptr || id[0] == '\0') return nullptr; + for (const AppDescriptor& d : descriptors_) { + if (core::equalsIgnoreCase(d.id, id)) return &d; } return nullptr; } std::vector AppRegistry::listByCategory(const char* category) const { + if (category == nullptr || category[0] == '\0') return descriptors_; std::vector filtered; - if (category == nullptr || category[0] == '\0') { - return descriptors_; - } - for (const AppDescriptor& descriptor : descriptors_) { - if (equalsIgnoreCase(descriptor.category, category)) { - filtered.push_back(descriptor); - } + for (const AppDescriptor& d : descriptors_) { + if (core::equalsIgnoreCase(d.category, category)) filtered.push_back(d); } return filtered; } @@ -151,15 +123,18 @@ const std::vector& AppRegistry::descriptors() const { return descriptors_; } +// ============================================================================ +// JSON parsing — called once at boot. DynamicJsonDocument(8192) intentional. +// ============================================================================ + bool AppRegistry::loadFromJson(const char* json_text) { - if (json_text == nullptr || json_text[0] == '\0') { - return false; - } + if (json_text == nullptr || json_text[0] == '\0') return false; + DynamicJsonDocument document(8192); - const DeserializationError error = deserializeJson(document, json_text); - if (error) { + if (deserializeJson(document, json_text) != DeserializationError::Ok) { return false; } + JsonArrayConst items; if (document.is()) { items = document.as(); @@ -168,47 +143,44 @@ bool AppRegistry::loadFromJson(const char* json_text) { } else { return false; } + descriptors_.clear(); for (JsonVariantConst item : items) { - if (!item.is()) { - continue; - } + if (!item.is()) continue; JsonObjectConst entry = item.as(); + const char* id = entry["id"] | ""; - if (id[0] == '\0') { - continue; + if (id[0] == '\0') continue; + + AppDescriptor d = {}; + core::copyText(d.id, sizeof(d.id), id); + core::copyText(d.title, sizeof(d.title), entry["title"] | id); + core::copyText(d.category, sizeof(d.category), entry["category"] | "misc"); + core::copyText(d.entry_screen, sizeof(d.entry_screen), entry["entry_screen"] | ""); + d.enabled = entry["enabled"] | true; + core::copyText(d.version, sizeof(d.version), entry["version"] | "1.0.0"); + + core::copyText(d.icon_path, sizeof(d.icon_path), entry["icon_path"] | ""); + if (d.icon_path[0] == '\0') { + char default_icon[96] = {}; + std::snprintf(default_icon, sizeof(default_icon), "/apps/%s/icon.png", d.id); + core::copyText(d.icon_path, sizeof(d.icon_path), default_icon); } - AppDescriptor descriptor = {}; - copyText(descriptor.id, sizeof(descriptor.id), id); - copyText(descriptor.title, sizeof(descriptor.title), entry["title"] | id); - copyText(descriptor.category, sizeof(descriptor.category), entry["category"] | "misc"); - copyText(descriptor.entry_screen, sizeof(descriptor.entry_screen), entry["entry_screen"] | ""); - descriptor.enabled = entry["enabled"] | true; - copyText(descriptor.version, sizeof(descriptor.version), entry["version"] | "1.0.0"); - copyText(descriptor.icon_path, sizeof(descriptor.icon_path), entry["icon_path"] | ""); - if (descriptor.icon_path[0] == '\0') { - char default_icon[96] = {0}; - std::snprintf(default_icon, sizeof(default_icon), "/apps/%s/icon.png", descriptor.id); - copyText(descriptor.icon_path, sizeof(descriptor.icon_path), default_icon); - } - copyText(descriptor.asset_manifest, sizeof(descriptor.asset_manifest), entry["asset_manifest"] | ""); - descriptor.supports_offline = entry["supports_offline"] | true; - descriptor.supports_streaming = entry["supports_streaming"] | false; - if (entry["required_capabilities"].is()) { - descriptor.required_capabilities = entry["required_capabilities"].as(); - } else if (entry["required_capabilities"].is()) { - descriptor.required_capabilities = parseCapabilityMask(entry["required_capabilities"] | ""); - } else { - descriptor.required_capabilities = parseCapabilityMask(entry["required_caps"] | ""); - } - if (entry["optional_capabilities"].is()) { - descriptor.optional_capabilities = entry["optional_capabilities"].as(); - } else if (entry["optional_capabilities"].is()) { - descriptor.optional_capabilities = parseCapabilityMask(entry["optional_capabilities"] | ""); - } else { - descriptor.optional_capabilities = parseCapabilityMask(entry["optional_caps"] | ""); - } - descriptors_.push_back(descriptor); + + core::copyText(d.asset_manifest, sizeof(d.asset_manifest), entry["asset_manifest"] | ""); + d.supports_offline = entry["supports_offline"] | true; + d.supports_streaming = entry["supports_streaming"] | false; + + // Capabilities: accept uint32 or CSV string, with legacy key aliases. + auto parseCap = [&](const char* key, const char* key_alt) -> uint32_t { + if (entry[key].is()) return entry[key].as(); + if (entry[key].is()) return parseCapabilityMask(entry[key] | ""); + return parseCapabilityMask(entry[key_alt] | ""); + }; + d.required_capabilities = parseCap("required_capabilities", "required_caps"); + d.optional_capabilities = parseCap("optional_capabilities", "optional_caps"); + + descriptors_.push_back(d); } return !descriptors_.empty(); } @@ -216,68 +188,49 @@ bool AppRegistry::loadFromJson(const char* json_text) { void AppRegistry::loadFallbackCatalog() { descriptors_.clear(); descriptors_.reserve(sizeof(kFallbackApps) / sizeof(kFallbackApps[0])); - for (const FallbackAppEntry& fallback : kFallbackApps) { - AppDescriptor descriptor = {}; - copyText(descriptor.id, sizeof(descriptor.id), fallback.id); - copyText(descriptor.title, sizeof(descriptor.title), fallback.title); - copyText(descriptor.category, sizeof(descriptor.category), fallback.category); - copyText(descriptor.entry_screen, sizeof(descriptor.entry_screen), fallback.entry_screen); - descriptor.enabled = fallback.enabled; - copyText(descriptor.version, sizeof(descriptor.version), "1.0.0"); - char icon_path[96] = {0}; - std::snprintf(icon_path, sizeof(icon_path), "/apps/%s/icon.png", fallback.id); - copyText(descriptor.icon_path, sizeof(descriptor.icon_path), icon_path); - copyText(descriptor.asset_manifest, sizeof(descriptor.asset_manifest), fallback.manifest); - descriptor.required_capabilities = fallback.required; - descriptor.optional_capabilities = fallback.optional; - descriptor.supports_offline = fallback.offline; - descriptor.supports_streaming = fallback.streaming; - descriptors_.push_back(descriptor); + for (const FallbackAppEntry& f : kFallbackApps) { + AppDescriptor d = {}; + core::copyText(d.id, sizeof(d.id), f.id); + core::copyText(d.title, sizeof(d.title), f.title); + core::copyText(d.category, sizeof(d.category), f.category); + core::copyText(d.entry_screen, sizeof(d.entry_screen), f.entry_screen); + d.enabled = f.enabled; + core::copyText(d.version, sizeof(d.version), "1.0.0"); + + char icon_path[96] = {}; + std::snprintf(icon_path, sizeof(icon_path), "/apps/%s/icon.png", f.id); + core::copyText(d.icon_path, sizeof(d.icon_path), icon_path); + core::copyText(d.asset_manifest, sizeof(d.asset_manifest), f.manifest); + + d.required_capabilities = f.required; + d.optional_capabilities = f.optional; + d.supports_offline = f.offline; + d.supports_streaming = f.streaming; + descriptors_.push_back(d); } } uint32_t AppRegistry::parseCapabilityMask(const char* csv_caps) { - if (csv_caps == nullptr || csv_caps[0] == '\0') { - return 0U; - } - char buffer[192] = {0}; - copyText(buffer, sizeof(buffer), csv_caps); + if (csv_caps == nullptr || csv_caps[0] == '\0') return 0U; + + char buffer[192] = {}; + core::copyText(buffer, sizeof(buffer), csv_caps); + uint32_t mask = 0U; char* token = std::strtok(buffer, ",| "); while (token != nullptr) { for (size_t i = 0U; token[i] != '\0'; ++i) { token[i] = static_cast(std::tolower(static_cast(token[i]))); } - if (std::strcmp(token, "cap_audio_out") == 0 || std::strcmp(token, "audio_out") == 0) { - mask |= CAP_AUDIO_OUT; - } else if (std::strcmp(token, "cap_audio_in") == 0 || std::strcmp(token, "audio_in") == 0) { - mask |= CAP_AUDIO_IN; - } else if (std::strcmp(token, "cap_camera") == 0 || std::strcmp(token, "camera") == 0) { - mask |= CAP_CAMERA; - } else if (std::strcmp(token, "cap_led") == 0 || std::strcmp(token, "led") == 0) { - mask |= CAP_LED; - } else if (std::strcmp(token, "cap_wifi") == 0 || std::strcmp(token, "wifi") == 0) { - mask |= CAP_WIFI; - } else if (std::strcmp(token, "cap_storage_sd") == 0 || std::strcmp(token, "storage_sd") == 0) { - mask |= CAP_STORAGE_SD; - } else if (std::strcmp(token, "cap_storage_fs") == 0 || std::strcmp(token, "storage_fs") == 0) { - mask |= CAP_STORAGE_FS; - } else if (std::strcmp(token, "cap_gpu_ui") == 0 || std::strcmp(token, "gpu_ui") == 0) { - mask |= CAP_GPU_UI; - } + if (std::strcmp(token, "cap_audio_out") == 0 || std::strcmp(token, "audio_out") == 0) mask |= CAP_AUDIO_OUT; + else if (std::strcmp(token, "cap_audio_in") == 0 || std::strcmp(token, "audio_in") == 0) mask |= CAP_AUDIO_IN; + else if (std::strcmp(token, "cap_camera") == 0 || std::strcmp(token, "camera") == 0) mask |= CAP_CAMERA; + else if (std::strcmp(token, "cap_led") == 0 || std::strcmp(token, "led") == 0) mask |= CAP_LED; + else if (std::strcmp(token, "cap_wifi") == 0 || std::strcmp(token, "wifi") == 0) mask |= CAP_WIFI; + else if (std::strcmp(token, "cap_storage_sd") == 0 || std::strcmp(token, "storage_sd") == 0) mask |= CAP_STORAGE_SD; + else if (std::strcmp(token, "cap_storage_fs") == 0 || std::strcmp(token, "storage_fs") == 0) mask |= CAP_STORAGE_FS; + else if (std::strcmp(token, "cap_gpu_ui") == 0 || std::strcmp(token, "gpu_ui") == 0) mask |= CAP_GPU_UI; token = std::strtok(nullptr, ",| "); } return mask; } - -void AppRegistry::copyText(char* out, size_t out_size, const char* text) { - if (out == nullptr || out_size == 0U) { - return; - } - if (text == nullptr) { - out[0] = '\0'; - return; - } - std::strncpy(out, text, out_size - 1U); - out[out_size - 1U] = '\0'; -} diff --git a/ui_freenove_allinone/src/app/app_runtime_manager.cpp b/ui_freenove_allinone/src/app/app_runtime_manager.cpp index c944c32..1018428 100644 --- a/ui_freenove_allinone/src/app/app_runtime_manager.cpp +++ b/ui_freenove_allinone/src/app/app_runtime_manager.cpp @@ -2,11 +2,11 @@ #include "app/app_runtime_manager.h" #include -#include #include "app/modules/app_modules.h" #include "audio_manager.h" #include "camera_manager.h" +#include "core/str_utils.h" #include "hardware_manager.h" #include "media_manager.h" #include "network_manager.h" @@ -20,281 +20,253 @@ namespace { -bool equalsIgnoreCase(const char* lhs, const char* rhs) { - if (lhs == nullptr || rhs == nullptr) { - return false; +// ============================================================================ +// Heap snapshot cache — ESP.getFreeHeap() is expensive; refresh every 1 s. +// ============================================================================ + +struct HeapCache { + uint32_t heap_free = 0U; + uint32_t psram_free = 0U; + uint32_t last_ms = 0U; + + void refresh() { + const uint32_t now = millis(); + if (now - last_ms < 1000U) return; + heap_free = ESP.getFreeHeap(); +#if defined(ARDUINO_ARCH_ESP32) + psram_free = heap_caps_get_free_size(MALLOC_CAP_SPIRAM); +#else + psram_free = 0U; +#endif + last_ms = now; } - for (size_t i = 0U;; ++i) { - const char a = lhs[i]; - const char b = rhs[i]; - if (a == '\0' && b == '\0') { - return true; - } - if (a == '\0' || b == '\0') { - return false; - } - const char lower_a = (a >= 'A' && a <= 'Z') ? static_cast(a - 'A' + 'a') : a; - const char lower_b = (b >= 'A' && b <= 'Z') ? static_cast(b - 'A' + 'a') : b; - if (lower_a != lower_b) { - return false; - } +}; + +HeapCache g_heap_cache; + +void updateRuntimePerfCounters(AppRuntimeStatus* status, uint32_t tick_us) { + if (status == nullptr) return; + if (tick_us > status->max_tick_us) { + status->max_tick_us = tick_us; } + status->avg_tick_us = (status->avg_tick_us == 0U) + ? tick_us + : ((status->avg_tick_us * 7U) + tick_us) / 8U; + g_heap_cache.refresh(); + status->heap_free = g_heap_cache.heap_free; + status->psram_free = g_heap_cache.psram_free; } +// ============================================================================ +// BasicAppModule — default behaviour for apps without a dedicated module. +// ID-based dispatch maps app IDs to hardware actions at start/end. +// ============================================================================ + class BasicAppModule : public IAppModule { public: - bool begin(const AppContext& context) override { - context_ = context; + bool begin(const AppContext& ctx) override { + ctx_ = ctx; status_ = {}; - if (context.descriptor != nullptr) { - copyText(status_.id, sizeof(status_.id), context.descriptor->id); + if (ctx.descriptor != nullptr) { + core::copyText(status_.id, sizeof(status_.id), ctx.descriptor->id); } - status_.state = AppRuntimeState::kStarting; + status_.state = AppRuntimeState::kStarting; status_.started_at_ms = millis(); - if (context.descriptor == nullptr) { - copyText(status_.last_error, sizeof(status_.last_error), "missing_descriptor"); + if (ctx.descriptor == nullptr) { + core::copyText(status_.last_error, sizeof(status_.last_error), "missing_descriptor"); status_.state = AppRuntimeState::kFailed; return false; } - const char* id = context.descriptor->id; + const char* id = ctx.descriptor->id; bool ok = true; - if (equalsIgnoreCase(id, "camera_video") || equalsIgnoreCase(id, "qr_scanner")) { - ok = (context.camera != nullptr) && context.camera->start(); - if (!ok) { - copyText(status_.last_error, sizeof(status_.last_error), "camera_start_failed"); - } - } else if (equalsIgnoreCase(id, "dictaphone")) { - ok = (context.media != nullptr) && context.media->startRecording(30U, nullptr); - if (!ok) { - copyText(status_.last_error, sizeof(status_.last_error), "record_start_failed"); - } - } else if (equalsIgnoreCase(id, "flashlight")) { - ok = (context.hardware != nullptr) && context.hardware->setManualLed(255U, 255U, 255U, 120U, false); - if (!ok) { - copyText(status_.last_error, sizeof(status_.last_error), "flashlight_start_failed"); - } - } else if (equalsIgnoreCase(id, "audio_player") || - equalsIgnoreCase(id, "audiobook_player") || - equalsIgnoreCase(id, "kids_webradio") || - equalsIgnoreCase(id, "kids_podcast") || - equalsIgnoreCase(id, "kids_music")) { - ok = (context.audio != nullptr) && context.audio->play("/music/boot_radio.mp3"); - if (!ok) { - copyText(status_.last_error, sizeof(status_.last_error), "audio_start_failed"); - } + if (core::equalsIgnoreCase(id, "camera_video") || + core::equalsIgnoreCase(id, "qr_scanner")) { + ok = (ctx.camera != nullptr) && ctx.camera->start(); + if (!ok) core::copyText(status_.last_error, sizeof(status_.last_error), "camera_start_failed"); + + } else if (core::equalsIgnoreCase(id, "dictaphone")) { + ok = (ctx.media != nullptr) && ctx.media->startRecording(30U, nullptr); + if (!ok) core::copyText(status_.last_error, sizeof(status_.last_error), "record_start_failed"); + + } else if (core::equalsIgnoreCase(id, "flashlight")) { + ok = (ctx.hardware != nullptr) && ctx.hardware->setManualLed(255U, 255U, 255U, 120U, false); + if (!ok) core::copyText(status_.last_error, sizeof(status_.last_error), "flashlight_start_failed"); + + } else if (core::equalsIgnoreCase(id, "audio_player") || + core::equalsIgnoreCase(id, "audiobook_player") || + core::equalsIgnoreCase(id, "kids_webradio") || + core::equalsIgnoreCase(id, "kids_podcast") || + core::equalsIgnoreCase(id, "kids_music")) { + ok = (ctx.audio != nullptr) && ctx.audio->play("/apps/audio_player/audio/default.mp3"); + if (!ok) core::copyText(status_.last_error, sizeof(status_.last_error), "audio_start_failed"); } - if (!ok) { - status_.state = AppRuntimeState::kFailed; - return false; - } - status_.state = AppRuntimeState::kRunning; - copyText(status_.last_event, sizeof(status_.last_event), "begin"); - return true; + status_.state = ok ? AppRuntimeState::kRunning : AppRuntimeState::kFailed; + if (ok) core::copyText(status_.last_event, sizeof(status_.last_event), "begin"); + return ok; } void tick(uint32_t now_ms) override { status_.last_tick_ms = now_ms; - status_.tick_count += 1U; + status_.tick_count += 1U; } void handleAction(const AppAction& action) override { - copyText(status_.last_event, sizeof(status_.last_event), action.name); - if (status_.state != AppRuntimeState::kRunning) { - return; - } - if (equalsIgnoreCase(action.name, "play")) { - if (context_.media != nullptr && action.payload[0] != '\0') { - if (!context_.media->play(action.payload, context_.audio)) { - copyText(status_.last_error, sizeof(status_.last_error), "play_failed"); + core::copyText(status_.last_event, sizeof(status_.last_event), action.name); + if (status_.state != AppRuntimeState::kRunning) return; + + if (core::equalsIgnoreCase(action.name, "play")) { + if (ctx_.media != nullptr && action.payload[0] != '\0') { + if (!ctx_.media->play(action.payload, ctx_.audio)) { + core::copyText(status_.last_error, sizeof(status_.last_error), "play_failed"); } } - return; - } - if (equalsIgnoreCase(action.name, "stop")) { - if (context_.media != nullptr) { - context_.media->stop(context_.audio); - } else if (context_.audio != nullptr) { - context_.audio->stop(); + } else if (core::equalsIgnoreCase(action.name, "stop")) { + if (ctx_.media != nullptr) { + ctx_.media->stop(ctx_.audio); + } else if (ctx_.audio != nullptr) { + ctx_.audio->stop(); } - return; - } - if (equalsIgnoreCase(action.name, "record_start")) { + } else if (core::equalsIgnoreCase(action.name, "record_start")) { uint16_t seconds = static_cast(std::strtoul(action.payload, nullptr, 10)); - if (seconds == 0U) { - seconds = 30U; + if (seconds == 0U) seconds = 30U; + if (ctx_.media != nullptr && !ctx_.media->startRecording(seconds, nullptr)) { + core::copyText(status_.last_error, sizeof(status_.last_error), "record_start_failed"); } - if (context_.media != nullptr && !context_.media->startRecording(seconds, nullptr)) { - copyText(status_.last_error, sizeof(status_.last_error), "record_start_failed"); - } - return; - } - if (equalsIgnoreCase(action.name, "record_stop")) { - if (context_.media != nullptr) { - context_.media->stopRecording(); - } - return; - } - if (equalsIgnoreCase(action.name, "snapshot")) { - if (context_.camera != nullptr) { + } else if (core::equalsIgnoreCase(action.name, "record_stop")) { + if (ctx_.media != nullptr) ctx_.media->stopRecording(); + } else if (core::equalsIgnoreCase(action.name, "snapshot")) { + if (ctx_.camera != nullptr) { String out_path; - if (!context_.camera->snapshotToFile(nullptr, &out_path)) { - copyText(status_.last_error, sizeof(status_.last_error), "snapshot_failed"); + if (!ctx_.camera->snapshotToFile(nullptr, &out_path)) { + core::copyText(status_.last_error, sizeof(status_.last_error), "snapshot_failed"); } } - return; - } - if (equalsIgnoreCase(action.name, "light_on")) { - if (context_.hardware != nullptr && - !context_.hardware->setManualLed(255U, 255U, 255U, 120U, false)) { - copyText(status_.last_error, sizeof(status_.last_error), "light_on_failed"); + } else if (core::equalsIgnoreCase(action.name, "light_on")) { + if (ctx_.hardware != nullptr && + !ctx_.hardware->setManualLed(255U, 255U, 255U, 120U, false)) { + core::copyText(status_.last_error, sizeof(status_.last_error), "light_on_failed"); } - return; - } - if (equalsIgnoreCase(action.name, "light_off")) { - if (context_.hardware != nullptr) { - context_.hardware->clearManualLed(); - } - return; + } else if (core::equalsIgnoreCase(action.name, "light_off")) { + if (ctx_.hardware != nullptr) ctx_.hardware->clearManualLed(); } } void end() override { - if (context_.descriptor == nullptr) { + if (ctx_.descriptor == nullptr) { status_.state = AppRuntimeState::kIdle; return; } - const char* id = context_.descriptor->id; - if (equalsIgnoreCase(id, "camera_video") || equalsIgnoreCase(id, "qr_scanner")) { - if (context_.camera != nullptr) { - context_.camera->stop(); - } - } else if (equalsIgnoreCase(id, "dictaphone")) { - if (context_.media != nullptr) { - context_.media->stopRecording(); - } - } else if (equalsIgnoreCase(id, "flashlight")) { - if (context_.hardware != nullptr) { - context_.hardware->clearManualLed(); - } - } else if (equalsIgnoreCase(id, "audio_player") || - equalsIgnoreCase(id, "audiobook_player") || - equalsIgnoreCase(id, "kids_webradio") || - equalsIgnoreCase(id, "kids_podcast") || - equalsIgnoreCase(id, "kids_music")) { - if (context_.audio != nullptr) { - context_.audio->stop(); - } + const char* id = ctx_.descriptor->id; + + if (core::equalsIgnoreCase(id, "camera_video") || + core::equalsIgnoreCase(id, "qr_scanner")) { + if (ctx_.camera != nullptr) ctx_.camera->stop(); + + } else if (core::equalsIgnoreCase(id, "dictaphone")) { + if (ctx_.media != nullptr) ctx_.media->stopRecording(); + + } else if (core::equalsIgnoreCase(id, "flashlight")) { + if (ctx_.hardware != nullptr) ctx_.hardware->clearManualLed(); + + } else if (core::equalsIgnoreCase(id, "audio_player") || + core::equalsIgnoreCase(id, "audiobook_player") || + core::equalsIgnoreCase(id, "kids_webradio") || + core::equalsIgnoreCase(id, "kids_podcast") || + core::equalsIgnoreCase(id, "kids_music")) { + if (ctx_.audio != nullptr) ctx_.audio->stop(); } + status_.state = AppRuntimeState::kIdle; - copyText(status_.last_event, sizeof(status_.last_event), "end"); + core::copyText(status_.last_event, sizeof(status_.last_event), "end"); } - AppRuntimeStatus status() const override { - return status_; - } + AppRuntimeStatus status() const override { return status_; } private: - static void copyText(char* out, size_t out_size, const char* text) { - if (out == nullptr || out_size == 0U) { - return; - } - if (text == nullptr) { - out[0] = '\0'; - return; - } - std::strncpy(out, text, out_size - 1U); - out[out_size - 1U] = '\0'; - } - - AppContext context_ = {}; + AppContext ctx_ = {}; AppRuntimeStatus status_ = {}; }; -void updateRuntimePerfCounters(AppRuntimeStatus* status, uint32_t tick_us) { - if (status == nullptr) { - return; - } - if (tick_us > status->max_tick_us) { - status->max_tick_us = tick_us; - } - if (status->avg_tick_us == 0U) { - status->avg_tick_us = tick_us; - } else { - status->avg_tick_us = ((status->avg_tick_us * 7U) + tick_us) / 8U; - } - status->heap_free = ESP.getFreeHeap(); -#if defined(ARDUINO_ARCH_ESP32) - status->psram_free = heap_caps_get_free_size(MALLOC_CAP_SPIRAM); -#else - status->psram_free = 0U; -#endif -} - } // namespace +// ============================================================================ +// AppRuntimeManager +// ============================================================================ + void AppRuntimeManager::configure(AppRegistry* registry, const AppContext& context) { - registry_ = registry; - context_ = context; + registry_ = registry; + context_ = context; module_.reset(); current_descriptor_ = nullptr; - status_ = {}; + status_ = {}; } bool AppRuntimeManager::startApp(const AppStartRequest& request, uint32_t now_ms) { if (registry_ == nullptr || request.id[0] == '\0') { - copyText(status_.last_error, sizeof(status_.last_error), "app_registry_unavailable"); + core::copyText(status_.last_error, sizeof(status_.last_error), "app_registry_unavailable"); status_.state = AppRuntimeState::kFailed; return false; } + const AppDescriptor* descriptor = registry_->find(request.id); if (descriptor == nullptr) { - copyText(status_.last_error, sizeof(status_.last_error), "app_not_found"); + core::copyText(status_.last_error, sizeof(status_.last_error), "app_not_found"); status_.state = AppRuntimeState::kFailed; return false; } if (!descriptor->enabled) { - copyText(status_.last_error, sizeof(status_.last_error), "app_disabled"); + core::copyText(status_.last_error, sizeof(status_.last_error), "app_disabled"); status_.state = AppRuntimeState::kFailed; return false; } - AppStopRequest stop_request = {}; - copyText(stop_request.id, sizeof(stop_request.id), descriptor->id); - copyText(stop_request.reason, sizeof(stop_request.reason), "switch"); - (void)stopApp(stop_request, now_ms); + // Stop any currently running app. + AppStopRequest stop_req = {}; + core::copyText(stop_req.id, sizeof(stop_req.id), descriptor->id); + core::copyText(stop_req.reason, sizeof(stop_req.reason), "switch"); + (void)stopApp(stop_req, now_ms); status_ = {}; - copyText(status_.id, sizeof(status_.id), descriptor->id); - copyText(status_.mode, sizeof(status_.mode), request.mode); - copyText(status_.source, sizeof(status_.source), request.source); - status_.state = AppRuntimeState::kStarting; - status_.started_at_ms = now_ms; + core::copyText(status_.id, sizeof(status_.id), descriptor->id); + core::copyText(status_.mode, sizeof(status_.mode), request.mode); + core::copyText(status_.source, sizeof(status_.source), request.source); + status_.state = AppRuntimeState::kStarting; + status_.started_at_ms = now_ms; status_.required_cap_mask = descriptor->required_capabilities; + if (context_.resource != nullptr) { const uint32_t req = descriptor->required_capabilities; if (appCapabilityMaskHas(req, CAP_AUDIO_IN)) { context_.resource->setProfile(runtime::resource::ResourceProfile::kGfxPlusMic); } else if (appCapabilityMaskHas(req, CAP_CAMERA)) { context_.resource->setProfile(runtime::resource::ResourceProfile::kGfxPlusCamSnapshot); + if (context_.hardware != nullptr) { + context_.hardware->setMicRuntimeEnabled(false); + } + if (context_.audio != nullptr) { + context_.audio->releaseOutputResources(); + } } } + status_.missing_cap_mask = evaluateMissingCapabilities(*descriptor); if (status_.missing_cap_mask != 0U) { - copyText(status_.last_error, sizeof(status_.last_error), "resource_busy"); - status_.state = AppRuntimeState::kFailed; + core::copyText(status_.last_error, sizeof(status_.last_error), "resource_busy"); + status_.state = AppRuntimeState::kFailed; current_descriptor_ = descriptor; return false; } + if (descriptor->supports_offline && descriptor->asset_manifest[0] != '\0' && context_.storage != nullptr && !context_.storage->fileExists(descriptor->asset_manifest)) { - copyText(status_.last_error, sizeof(status_.last_error), "missing_asset"); - status_.state = AppRuntimeState::kFailed; + core::copyText(status_.last_error, sizeof(status_.last_error), "missing_asset"); + status_.state = AppRuntimeState::kFailed; current_descriptor_ = descriptor; return false; } @@ -303,21 +275,24 @@ bool AppRuntimeManager::startApp(const AppStartRequest& request, uint32_t now_ms if (!module_) { module_.reset(new BasicAppModule()); } - AppContext run_context = context_; - run_context.descriptor = descriptor; - const bool ok = module_->begin(run_context); + + AppContext run_ctx = context_; + run_ctx.descriptor = descriptor; + const bool ok = module_->begin(run_ctx); + status_ = module_->status(); - copyText(status_.mode, sizeof(status_.mode), request.mode); - copyText(status_.source, sizeof(status_.source), request.source); + core::copyText(status_.mode, sizeof(status_.mode), request.mode); + core::copyText(status_.source, sizeof(status_.source), request.source); status_.required_cap_mask = descriptor->required_capabilities; - status_.missing_cap_mask = 0U; + status_.missing_cap_mask = 0U; updateRuntimePerfCounters(&status_, 0U); current_descriptor_ = descriptor; + if (ok && context_.ui != nullptr && descriptor->entry_screen[0] != '\0') { - UiSceneFrame frame = {}; - frame.screen_scene_id = descriptor->entry_screen; - frame.step_id = descriptor->id; - frame.audio_playing = (context_.audio != nullptr) ? context_.audio->isPlaying() : false; + UiSceneFrame frame = {}; + frame.screen_scene_id = descriptor->entry_screen; + frame.step_id = descriptor->id; + frame.audio_playing = (context_.audio != nullptr) && context_.audio->isPlaying(); context_.ui->submitSceneFrame(frame); } return ok; @@ -326,68 +301,81 @@ bool AppRuntimeManager::startApp(const AppStartRequest& request, uint32_t now_ms bool AppRuntimeManager::stopApp(const AppStopRequest& request, uint32_t now_ms) { (void)now_ms; (void)request; - if (!module_) { - return true; - } + if (!module_) return true; + const bool restoring_camera_sidecars = + current_descriptor_ != nullptr && + appCapabilityMaskHas(current_descriptor_->required_capabilities, CAP_CAMERA); + status_.state = AppRuntimeState::kStopping; module_->end(); status_ = module_->status(); module_.reset(); current_descriptor_ = nullptr; - copyText(status_.id, sizeof(status_.id), ""); - status_.missing_cap_mask = 0U; + core::copyText(status_.id, sizeof(status_.id), ""); + status_.missing_cap_mask = 0U; status_.required_cap_mask = 0U; + if (context_.ui != nullptr) { - UiSceneFrame frame = {}; - frame.screen_scene_id = "SCENE_READY"; - frame.step_id = "APP_IDLE"; - frame.audio_playing = (context_.audio != nullptr) ? context_.audio->isPlaying() : false; + UiSceneFrame frame = {}; + frame.screen_scene_id = "SCENE_READY"; + frame.step_id = "APP_IDLE"; + frame.audio_playing = (context_.audio != nullptr) && context_.audio->isPlaying(); context_.ui->submitSceneFrame(frame); } + if (restoring_camera_sidecars && context_.audio != nullptr) { + context_.audio->restoreOutputResources(); + } return true; } bool AppRuntimeManager::handleAction(const AppAction& action, uint32_t now_ms) { (void)now_ms; if (!module_) { - copyText(status_.last_error, sizeof(status_.last_error), "no_app_running"); + core::copyText(status_.last_error, sizeof(status_.last_error), "no_app_running"); status_.state = AppRuntimeState::kFailed; return false; } - if (action.id[0] != '\0' && !equalsIgnoreCase(action.id, status_.id)) { - copyText(status_.last_error, sizeof(status_.last_error), "app_id_mismatch"); + if (action.id[0] != '\0' && !core::equalsIgnoreCase(action.id, status_.id)) { + core::copyText(status_.last_error, sizeof(status_.last_error), "app_id_mismatch"); return false; } - char mode[sizeof(status_.mode)] = {0}; - char source[sizeof(status_.source)] = {0}; - copyText(mode, sizeof(mode), status_.mode); - copyText(source, sizeof(source), status_.source); + + char mode[sizeof(status_.mode)] = {}; + char source[sizeof(status_.source)] = {}; + core::copyText(mode, sizeof(mode), status_.mode); + core::copyText(source, sizeof(source), status_.source); + module_->handleAction(action); status_ = module_->status(); - copyText(status_.mode, sizeof(status_.mode), mode); - copyText(status_.source, sizeof(status_.source), source); - status_.required_cap_mask = (current_descriptor_ != nullptr) ? current_descriptor_->required_capabilities : 0U; - status_.missing_cap_mask = (current_descriptor_ != nullptr) ? evaluateMissingCapabilities(*current_descriptor_) : 0U; + core::copyText(status_.mode, sizeof(status_.mode), mode); + core::copyText(status_.source, sizeof(status_.source), source); + status_.required_cap_mask = (current_descriptor_ != nullptr) + ? current_descriptor_->required_capabilities : 0U; + status_.missing_cap_mask = (current_descriptor_ != nullptr) + ? evaluateMissingCapabilities(*current_descriptor_) : 0U; updateRuntimePerfCounters(&status_, 0U); return true; } void AppRuntimeManager::tick(uint32_t now_ms) { - if (!module_) { - return; - } - char mode[sizeof(status_.mode)] = {0}; - char source[sizeof(status_.source)] = {0}; - copyText(mode, sizeof(mode), status_.mode); - copyText(source, sizeof(source), status_.source); - const uint32_t start_us = micros(); + if (!module_) return; + + char mode[sizeof(status_.mode)] = {}; + char source[sizeof(status_.source)] = {}; + core::copyText(mode, sizeof(mode), status_.mode); + core::copyText(source, sizeof(source), status_.source); + + const uint32_t start_us = micros(); module_->tick(now_ms); const uint32_t elapsed_us = micros() - start_us; + status_ = module_->status(); - copyText(status_.mode, sizeof(status_.mode), mode); - copyText(status_.source, sizeof(status_.source), source); - status_.required_cap_mask = (current_descriptor_ != nullptr) ? current_descriptor_->required_capabilities : 0U; - status_.missing_cap_mask = (current_descriptor_ != nullptr) ? evaluateMissingCapabilities(*current_descriptor_) : 0U; + core::copyText(status_.mode, sizeof(status_.mode), mode); + core::copyText(status_.source, sizeof(status_.source), source); + status_.required_cap_mask = (current_descriptor_ != nullptr) + ? current_descriptor_->required_capabilities : 0U; + status_.missing_cap_mask = (current_descriptor_ != nullptr) + ? evaluateMissingCapabilities(*current_descriptor_) : 0U; updateRuntimePerfCounters(&status_, elapsed_us); } @@ -402,87 +390,48 @@ const AppDescriptor* AppRuntimeManager::currentDescriptor() const { uint32_t AppRuntimeManager::evaluateMissingCapabilities(const AppDescriptor& descriptor) const { uint32_t missing = 0U; const uint32_t req = descriptor.required_capabilities; - runtime::resource::ResourceCoordinator* resource = context_.resource; + runtime::resource::ResourceCoordinator* res = context_.resource; - if (appCapabilityMaskHas(req, CAP_AUDIO_OUT) && context_.audio == nullptr) { - missing |= CAP_AUDIO_OUT; - } - if (appCapabilityMaskHas(req, CAP_AUDIO_OUT) && resource != nullptr && - !resource->allowsCapability(runtime::resource::ResourceCapability::kAudioOut)) { - missing |= CAP_AUDIO_OUT; + auto lacking = [&](runtime::resource::ResourceCapability cap) -> bool { + return res != nullptr && !res->allowsCapability(cap); + }; + + if (appCapabilityMaskHas(req, CAP_AUDIO_OUT)) { + if (context_.audio == nullptr || lacking(runtime::resource::ResourceCapability::kAudioOut)) + missing |= CAP_AUDIO_OUT; } if (appCapabilityMaskHas(req, CAP_AUDIO_IN)) { - const bool has_audio_in = (context_.media != nullptr) || - (context_.hardware != nullptr && context_.hardware->snapshotRef().mic_ready); - if (!has_audio_in) { + const bool has = (context_.media != nullptr) || + (context_.hardware != nullptr && context_.hardware->snapshotRef().mic_ready); + if (!has || lacking(runtime::resource::ResourceCapability::kAudioIn)) missing |= CAP_AUDIO_IN; - } - if (resource != nullptr && - !resource->allowsCapability(runtime::resource::ResourceCapability::kAudioIn)) { - missing |= CAP_AUDIO_IN; - } } if (appCapabilityMaskHas(req, CAP_CAMERA)) { - const bool has_camera = (context_.camera != nullptr && context_.camera->snapshot().supported); - const bool allowed_by_profile = - (resource == nullptr) || resource->allowsCapability(runtime::resource::ResourceCapability::kCamera); - if (!has_camera || !allowed_by_profile) { + const bool has = (context_.camera != nullptr && context_.camera->snapshot().supported); + if (!has || lacking(runtime::resource::ResourceCapability::kCamera)) missing |= CAP_CAMERA; - } } if (appCapabilityMaskHas(req, CAP_LED)) { - const bool has_led = (context_.hardware != nullptr && context_.hardware->snapshotRef().ws2812_ready); - if (!has_led) { + const bool has = (context_.hardware != nullptr && context_.hardware->snapshotRef().ws2812_ready); + if (!has || lacking(runtime::resource::ResourceCapability::kLed)) missing |= CAP_LED; - } - if (resource != nullptr && - !resource->allowsCapability(runtime::resource::ResourceCapability::kLed)) { - missing |= CAP_LED; - } } if (appCapabilityMaskHas(req, CAP_WIFI)) { - if (context_.network == nullptr) { + if (context_.network == nullptr || lacking(runtime::resource::ResourceCapability::kWifi)) missing |= CAP_WIFI; - } - if (resource != nullptr && - !resource->allowsCapability(runtime::resource::ResourceCapability::kWifi)) { - missing |= CAP_WIFI; - } } - if (appCapabilityMaskHas(req, CAP_STORAGE_SD) && - (context_.storage == nullptr || !context_.storage->hasSdCard())) { - missing |= CAP_STORAGE_SD; + if (appCapabilityMaskHas(req, CAP_STORAGE_SD)) { + const bool has = (context_.storage != nullptr && context_.storage->hasSdCard()); + if (!has || lacking(runtime::resource::ResourceCapability::kStorageSd)) + missing |= CAP_STORAGE_SD; } - if (appCapabilityMaskHas(req, CAP_STORAGE_SD) && resource != nullptr && - !resource->allowsCapability(runtime::resource::ResourceCapability::kStorageSd)) { - missing |= CAP_STORAGE_SD; + if (appCapabilityMaskHas(req, CAP_STORAGE_FS)) { + if (context_.storage == nullptr || lacking(runtime::resource::ResourceCapability::kStorageFs)) + missing |= CAP_STORAGE_FS; } - if (appCapabilityMaskHas(req, CAP_STORAGE_FS) && context_.storage == nullptr) { - missing |= CAP_STORAGE_FS; + if (appCapabilityMaskHas(req, CAP_GPU_UI)) { + if (context_.ui == nullptr || lacking(runtime::resource::ResourceCapability::kGpuUi)) + missing |= CAP_GPU_UI; } - if (appCapabilityMaskHas(req, CAP_STORAGE_FS) && resource != nullptr && - !resource->allowsCapability(runtime::resource::ResourceCapability::kStorageFs)) { - missing |= CAP_STORAGE_FS; - } - if (appCapabilityMaskHas(req, CAP_GPU_UI) && context_.ui == nullptr) { - missing |= CAP_GPU_UI; - } - if (appCapabilityMaskHas(req, CAP_GPU_UI) && resource != nullptr && - !resource->allowsCapability(runtime::resource::ResourceCapability::kGpuUi)) { - missing |= CAP_GPU_UI; - } - return missing; } - -void AppRuntimeManager::copyText(char* out, size_t out_size, const char* text) { - if (out == nullptr || out_size == 0U) { - return; - } - if (text == nullptr) { - out[0] = '\0'; - return; - } - std::strncpy(out, text, out_size - 1U); - out[out_size - 1U] = '\0'; -} diff --git a/ui_freenove_allinone/src/app/app_timer.cpp b/ui_freenove_allinone/src/app/app_timer.cpp index 72868bc..4e8236e 100644 --- a/ui_freenove_allinone/src/app/app_timer.cpp +++ b/ui_freenove_allinone/src/app/app_timer.cpp @@ -1,6 +1,7 @@ // app_timer.cpp - Timer Implementation #include "app/app_timer.h" -#include "hardware_manager.h" +#include "audio_manager.h" +#include "core/str_utils.h" AppTimer g_app_timer; @@ -44,7 +45,7 @@ void AppTimer::onAction(const AppAction& action) { Serial.printf("[APP_TIMER] Action: %s payload=%s\n", action.name, action.payload); if (strcmp(action.name, "start") == 0) { - uint32_t seconds = atoi(action.payload); + uint32_t seconds = static_cast(strtol(action.payload, nullptr, 10)); startCountdown(seconds); } else if (strcmp(action.name, "pause") == 0) { pause(); @@ -81,7 +82,7 @@ void AppTimer::reset() { running_ = false; remaining_ms_ = 0; total_ms_ = 0; - strncpy(display_, "00:00", sizeof(display_) - 1); + core::copyText(display_, sizeof(display_), "00:00"); } void AppTimer::updateDisplay() { @@ -92,14 +93,9 @@ void AppTimer::updateDisplay() { void AppTimer::onTimeout() { Serial.println("[APP_TIMER] TIMEOUT - Playing alarm"); - - // Play beep alarm if buzzer available - if (context_.hardware) { - for (int i = 0; i < 5; i++) { - // TODO: Use buzzer/audio to play alarm sound - // context_.hardware->buzzer(1000, 100); // Frequency, duration - delay(100); - } + + if (context_.audio) { + context_.audio->play("/sounds/alarm.wav"); } updateDisplay(); diff --git a/ui_freenove_allinone/src/app/file_share_service.cpp b/ui_freenove_allinone/src/app/file_share_service.cpp index 8fbe5c0..a2673b2 100644 --- a/ui_freenove_allinone/src/app/file_share_service.cpp +++ b/ui_freenove_allinone/src/app/file_share_service.cpp @@ -21,6 +21,7 @@ #endif #include +#include "core/str_utils.h" namespace { @@ -72,13 +73,13 @@ bool ensureDir(const char* path) { } // namespace bool FileShareService::begin(const char* host_name, const char* instance_name) { - copyText(host_name_, sizeof(host_name_), host_name); - copyText(instance_name_, sizeof(instance_name_), instance_name); + core::copyText(host_name_, sizeof(host_name_), host_name); + core::copyText(instance_name_, sizeof(instance_name_), instance_name); if (host_name_[0] == '\0') { - copyText(host_name_, sizeof(host_name_), "zacus-freenove"); + core::copyText(host_name_, sizeof(host_name_), "zacus-freenove"); } if (instance_name_[0] == '\0') { - copyText(instance_name_, sizeof(instance_name_), "zacus-device"); + core::copyText(instance_name_, sizeof(instance_name_), "zacus-device"); } ensureSharedDirs(); #if ZACUS_HAS_MDNS @@ -128,9 +129,9 @@ uint8_t FileShareService::discoverPeers(PeerInfo* out_peers, uint8_t max_peers) } for (int i = 0; i < found && count < max_peers; ++i) { PeerInfo& peer = out_peers[count]; - copyText(peer.instance, sizeof(peer.instance), MDNS.hostname(i).c_str()); - copyText(peer.host, sizeof(peer.host), MDNS.hostname(i).c_str()); - copyText(peer.ip, sizeof(peer.ip), MDNS.IP(i).toString().c_str()); + core::copyText(peer.instance, sizeof(peer.instance), MDNS.hostname(i).c_str()); + core::copyText(peer.host, sizeof(peer.host), MDNS.hostname(i).c_str()); + core::copyText(peer.ip, sizeof(peer.ip), MDNS.IP(i).toString().c_str()); peer.port = static_cast(MDNS.port(i)); ++count; } @@ -387,18 +388,6 @@ bool FileShareService::resolveIncomingPath(const char* requested_path, String* o return true; } -void FileShareService::copyText(char* out, size_t out_size, const char* text) { - if (out == nullptr || out_size == 0U) { - return; - } - if (text == nullptr) { - out[0] = '\0'; - return; - } - std::strncpy(out, text, out_size - 1U); - out[out_size - 1U] = '\0'; -} - bool FileShareService::ensureSharedDirs() const { return ensureDir("/apps") && ensureDir(kShareRoot) && ensureDir(kIncomingRoot); } diff --git a/ui_freenove_allinone/src/app/modules/app_modules.cpp b/ui_freenove_allinone/src/app/modules/app_modules.cpp index 1d97b1e..57f0290 100644 --- a/ui_freenove_allinone/src/app/modules/app_modules.cpp +++ b/ui_freenove_allinone/src/app/modules/app_modules.cpp @@ -8,6 +8,7 @@ #include #include #include +#include "core/str_utils.h" #include "audio_manager.h" #include "camera_manager.h" @@ -26,37 +27,28 @@ namespace app::modules { namespace { -void copyText(char* out, size_t out_size, const char* text) { - if (out == nullptr || out_size == 0U) { - return; - } - if (text == nullptr) { - out[0] = '\0'; - return; - } - std::strncpy(out, text, out_size - 1U); - out[out_size - 1U] = '\0'; -} +constexpr const char* kSharedBundledAudioTrack = "/apps/audio_player/audio/default.mp3"; -bool equalsIgnoreCase(const char* lhs, const char* rhs) { - if (lhs == nullptr || rhs == nullptr) { - return false; +const char* defaultBundledAudioForApp(const char* app_id) { + if (app_id == nullptr || app_id[0] == '\0') { + return kSharedBundledAudioTrack; } - for (size_t i = 0U;; ++i) { - const char a = lhs[i]; - const char b = rhs[i]; - if (a == '\0' && b == '\0') { - return true; - } - if (a == '\0' || b == '\0') { - return false; - } - const char la = (a >= 'A' && a <= 'Z') ? static_cast(a - 'A' + 'a') : a; - const char lb = (b >= 'A' && b <= 'Z') ? static_cast(b - 'A' + 'a') : b; - if (la != lb) { - return false; - } + if (core::equalsIgnoreCase(app_id, "audio_player")) { + return "/apps/audio_player/audio/default.mp3"; } + if (core::equalsIgnoreCase(app_id, "audiobook_player")) { + return "/apps/audiobook_player/audio/default.mp3"; + } + if (core::equalsIgnoreCase(app_id, "kids_music")) { + return "/apps/kids_music/audio/default.mp3"; + } + if (core::equalsIgnoreCase(app_id, "kids_webradio")) { + return "/apps/kids_webradio/audio/default.mp3"; + } + if (core::equalsIgnoreCase(app_id, "kids_podcast")) { + return "/apps/kids_podcast/audio/default.mp3"; + } + return kSharedBundledAudioTrack; } bool ensureDir(const char* path) { @@ -142,14 +134,14 @@ bool endsWithIgnoreCase(const char* text, const char* suffix) { return false; } const char* tail = text + (text_len - suffix_len); - return equalsIgnoreCase(tail, suffix); + return core::equalsIgnoreCase(tail, suffix); } bool parseJsonPayload(const AppAction& action, DynamicJsonDocument* out) { if (out == nullptr) { return false; } - const bool looks_json = (action.content_type[0] != '\0' && equalsIgnoreCase(action.content_type, "application/json")) || + const bool looks_json = (action.content_type[0] != '\0' && core::equalsIgnoreCase(action.content_type, "application/json")) || action.payload[0] == '{' || action.payload[0] == '['; if (!looks_json || action.payload[0] == '\0') { return false; @@ -164,11 +156,11 @@ class ModuleBase : public IAppModule { context_ = context; status_ = {}; if (context.descriptor != nullptr) { - copyText(status_.id, sizeof(status_.id), context.descriptor->id); + core::copyText(status_.id, sizeof(status_.id), context.descriptor->id); } status_.state = AppRuntimeState::kRunning; status_.started_at_ms = millis(); - copyText(status_.last_event, sizeof(status_.last_event), "begin"); + core::copyText(status_.last_event, sizeof(status_.last_event), "begin"); return context.descriptor != nullptr; } @@ -178,12 +170,12 @@ class ModuleBase : public IAppModule { } void handleAction(const AppAction& action) override { - copyText(status_.last_event, sizeof(status_.last_event), action.name); + core::copyText(status_.last_event, sizeof(status_.last_event), action.name); } void end() override { status_.state = AppRuntimeState::kIdle; - copyText(status_.last_event, sizeof(status_.last_event), "end"); + core::copyText(status_.last_event, sizeof(status_.last_event), "end"); } AppRuntimeStatus status() const override { @@ -192,7 +184,7 @@ class ModuleBase : public IAppModule { protected: void setError(const char* error) { - copyText(status_.last_error, sizeof(status_.last_error), error); + core::copyText(status_.last_error, sizeof(status_.last_error), error); } AppContext context_ = {}; @@ -215,8 +207,10 @@ class AudioPlayerModule : public ModuleBase { void handleAction(const AppAction& action) override { ModuleBase::handleAction(action); - if (equalsIgnoreCase(action.name, "play")) { - const char* path = (action.payload[0] != '\0') ? action.payload : "/music/boot_radio.mp3"; + if (core::equalsIgnoreCase(action.name, "play")) { + const char* path = (action.payload[0] != '\0') + ? action.payload + : defaultBundledAudioForApp((context_.descriptor != nullptr) ? context_.descriptor->id : nullptr); if (context_.storage != nullptr && !context_.storage->fileExists(path)) { setError("missing_asset"); return; @@ -229,7 +223,7 @@ class AudioPlayerModule : public ModuleBase { } return; } - if (equalsIgnoreCase(action.name, "play_url")) { + if (core::equalsIgnoreCase(action.name, "play_url")) { if (action.payload[0] == '\0') { setError("missing_asset"); return; @@ -243,7 +237,7 @@ class AudioPlayerModule : public ModuleBase { return; } } - if (!context_.audio->playUrl(action.payload)) { + if (!context_.audio->play(action.payload)) { if (!playOfflineFallback()) { setError("network_unavailable"); } @@ -253,7 +247,7 @@ class AudioPlayerModule : public ModuleBase { } return; } - if (equalsIgnoreCase(action.name, "pause")) { + if (core::equalsIgnoreCase(action.name, "pause")) { if (!paused_url_.isEmpty()) { context_.audio->stop(); } else { @@ -262,10 +256,10 @@ class AudioPlayerModule : public ModuleBase { } return; } - if (equalsIgnoreCase(action.name, "resume")) { + if (core::equalsIgnoreCase(action.name, "resume")) { bool ok = false; if (!paused_url_.isEmpty()) { - ok = context_.audio->playUrl(paused_url_.c_str()); + ok = context_.audio->play(paused_url_.c_str()); } else if (!paused_track_.isEmpty()) { ok = context_.media->play(paused_track_.c_str(), context_.audio); } @@ -274,17 +268,17 @@ class AudioPlayerModule : public ModuleBase { } return; } - if (equalsIgnoreCase(action.name, "stop")) { + if (core::equalsIgnoreCase(action.name, "stop")) { context_.media->stop(context_.audio); return; } - if (equalsIgnoreCase(action.name, "set_volume")) { + if (core::equalsIgnoreCase(action.name, "set_volume")) { const uint32_t volume = parseUint(action.payload, context_.audio->volume()); context_.audio->setVolume(static_cast(volume > 100U ? 100U : volume)); return; } - if (equalsIgnoreCase(action.name, "next") || equalsIgnoreCase(action.name, "prev")) { - copyText(status_.last_event, sizeof(status_.last_event), "playlist_not_configured"); + if (core::equalsIgnoreCase(action.name, "next") || core::equalsIgnoreCase(action.name, "prev")) { + core::copyText(status_.last_event, sizeof(status_.last_event), "playlist_not_configured"); return; } } @@ -298,13 +292,13 @@ class AudioPlayerModule : public ModuleBase { String candidates[3]; candidates[0] = String("/apps/") + app_id + "/audio/offline.mp3"; candidates[1] = String("/apps/") + app_id + "/audio/default.mp3"; - candidates[2] = "/music/boot_radio.mp3"; + candidates[2] = kSharedBundledAudioTrack; for (const String& candidate : candidates) { if (!context_.storage->fileExists(candidate.c_str())) { continue; } if (context_.media->play(candidate.c_str(), context_.audio)) { - copyText(status_.last_event, sizeof(status_.last_event), "offline_fallback"); + core::copyText(status_.last_event, sizeof(status_.last_event), "offline_fallback"); setError(""); return true; } @@ -333,12 +327,12 @@ class AudiobookModule : public ModuleBase { void handleAction(const AppAction& action) override { ModuleBase::handleAction(action); - if (equalsIgnoreCase(action.name, "open_book")) { + if (core::equalsIgnoreCase(action.name, "open_book")) { current_book_ = action.payload; saveProgress(); return; } - if (equalsIgnoreCase(action.name, "play")) { + if (core::equalsIgnoreCase(action.name, "play")) { const char* target = current_book_.isEmpty() ? action.payload : current_book_.c_str(); if (target == nullptr || target[0] == '\0') { setError("missing_asset"); @@ -352,7 +346,7 @@ class AudiobookModule : public ModuleBase { return; } } - if (!context_.audio->playUrl(target)) { + if (!context_.audio->play(target)) { setError("network_unavailable"); } return; @@ -366,27 +360,27 @@ class AudiobookModule : public ModuleBase { } return; } - if (equalsIgnoreCase(action.name, "pause")) { + if (core::equalsIgnoreCase(action.name, "pause")) { context_.audio->stop(); saveProgress(); return; } - if (equalsIgnoreCase(action.name, "stop")) { + if (core::equalsIgnoreCase(action.name, "stop")) { context_.media->stop(context_.audio); saveProgress(); return; } - if (equalsIgnoreCase(action.name, "seek_ms")) { + if (core::equalsIgnoreCase(action.name, "seek_ms")) { position_ms_ = parseUint(action.payload, position_ms_); saveProgress(); return; } - if (equalsIgnoreCase(action.name, "bookmark_set")) { + if (core::equalsIgnoreCase(action.name, "bookmark_set")) { bookmark_ms_ = parseUint(action.payload, position_ms_); saveProgress(); return; } - if (equalsIgnoreCase(action.name, "bookmark_go")) { + if (core::equalsIgnoreCase(action.name, "bookmark_go")) { position_ms_ = bookmark_ms_; saveProgress(); return; @@ -445,15 +439,23 @@ class CameraVideoModule : public ModuleBase { status_.state = AppRuntimeState::kFailed; return false; } - if (!context.camera->startRecorderSession()) { + bool camera_ready = context.camera->startRecorderSession(); + preview_on_ = camera_ready; + if (!camera_ready) { + camera_ready = context.camera->start(); + preview_on_ = false; + } + if (!camera_ready) { setError("camera_start_failed"); status_.state = AppRuntimeState::kFailed; return false; } - preview_on_ = true; clip_active_ = false; clip_frames_.clear(); setError(""); + core::copyText(status_.last_event, + sizeof(status_.last_event), + preview_on_ ? "preview_ready" : "snapshot_ready"); updateStatusEvent(); return true; } @@ -479,11 +481,11 @@ class CameraVideoModule : public ModuleBase { void handleAction(const AppAction& action) override { ModuleBase::handleAction(action); - if (equalsIgnoreCase(action.name, "status")) { + if (core::equalsIgnoreCase(action.name, "status")) { updateStatusEvent(); return; } - if (equalsIgnoreCase(action.name, "preview_on")) { + if (core::equalsIgnoreCase(action.name, "preview_on")) { if (!context_.camera->start()) { setError("preview_on_failed"); } else { @@ -493,26 +495,26 @@ class CameraVideoModule : public ModuleBase { } return; } - if (equalsIgnoreCase(action.name, "preview_off")) { + if (core::equalsIgnoreCase(action.name, "preview_off")) { context_.camera->stop(); preview_on_ = false; setError(""); updateStatusEvent(); return; } - if (equalsIgnoreCase(action.name, "snapshot")) { + if (core::equalsIgnoreCase(action.name, "snapshot")) { String out_path; if (!context_.camera->snapshotToFile(nullptr, &out_path)) { setError("snapshot_failed"); } else { char event[40] = {0}; std::snprintf(event, sizeof(event), "snap=%u", static_cast(out_path.length())); - copyText(status_.last_event, sizeof(status_.last_event), event); + core::copyText(status_.last_event, sizeof(status_.last_event), event); setError(""); } return; } - if (equalsIgnoreCase(action.name, "clip_start")) { + if (core::equalsIgnoreCase(action.name, "clip_start")) { clip_active_ = true; clip_frames_.clear(); clip_started_ms_ = millis(); @@ -521,13 +523,13 @@ class CameraVideoModule : public ModuleBase { updateStatusEvent(); return; } - if (equalsIgnoreCase(action.name, "clip_stop")) { + if (core::equalsIgnoreCase(action.name, "clip_stop")) { stopClipAndPersist(); setError(""); updateStatusEvent(); return; } - if (equalsIgnoreCase(action.name, "list_media")) { + if (core::equalsIgnoreCase(action.name, "list_media")) { static constexpr int kMaxListItems = 12; String items[kMaxListItems]; const int count = context_.camera->recorderListPhotos(items, kMaxListItems, true); @@ -536,17 +538,17 @@ class CameraVideoModule : public ModuleBase { } else { char event[40] = {0}; std::snprintf(event, sizeof(event), "list=%d", count); - copyText(status_.last_event, sizeof(status_.last_event), event); + core::copyText(status_.last_event, sizeof(status_.last_event), event); setError(""); } return; } - if (equalsIgnoreCase(action.name, "delete_media")) { + if (core::equalsIgnoreCase(action.name, "delete_media")) { if (action.payload[0] == '\0' || !context_.camera->recorderRemoveFile(action.payload)) { setError("delete_media_failed"); } else { setError(""); - copyText(status_.last_event, sizeof(status_.last_event), "delete_ok"); + core::copyText(status_.last_event, sizeof(status_.last_event), "delete_ok"); } return; } @@ -598,7 +600,7 @@ class CameraVideoModule : public ModuleBase { preview_on_ ? 1U : 0U, clip_active_ ? 1U : 0U, static_cast(clip_frames_.size())); - copyText(status_.last_event, sizeof(status_.last_event), event); + core::copyText(status_.last_event, sizeof(status_.last_event), event); } bool preview_on_ = false; @@ -623,7 +625,7 @@ class QrScannerModule : public ModuleBase { // Ensure CameraManager is not already holding the camera driver. context.camera->stop(); scanning_ = false; - copyText(last_type_, sizeof(last_type_), "none"); + core::copyText(last_type_, sizeof(last_type_), "none"); setError(""); updateStatusEvent(); return true; @@ -631,23 +633,23 @@ class QrScannerModule : public ModuleBase { void handleAction(const AppAction& action) override { ModuleBase::handleAction(action); - if (equalsIgnoreCase(action.name, "status")) { + if (core::equalsIgnoreCase(action.name, "status")) { updateStatusEvent(); return; } - if (equalsIgnoreCase(action.name, "scan_start")) { + if (core::equalsIgnoreCase(action.name, "scan_start")) { scanning_ = true; setError(""); updateStatusEvent(); return; } - if (equalsIgnoreCase(action.name, "scan_stop")) { + if (core::equalsIgnoreCase(action.name, "scan_stop")) { scanning_ = false; setError(""); updateStatusEvent(); return; } - if (equalsIgnoreCase(action.name, "scan_once") || equalsIgnoreCase(action.name, "scan_payload")) { + if (core::equalsIgnoreCase(action.name, "scan_once") || core::equalsIgnoreCase(action.name, "scan_payload")) { scanning_ = false; classifyPayload(action.payload); setError(""); @@ -664,18 +666,18 @@ class QrScannerModule : public ModuleBase { private: void classifyPayload(const char* payload) { if (payload == nullptr || payload[0] == '\0') { - copyText(last_type_, sizeof(last_type_), "unknown"); + core::copyText(last_type_, sizeof(last_type_), "unknown"); return; } if (std::strncmp(payload, "http://", 7U) == 0 || std::strncmp(payload, "https://", 8U) == 0) { - copyText(last_type_, sizeof(last_type_), "url"); + core::copyText(last_type_, sizeof(last_type_), "url"); return; } if (std::strncmp(payload, "app:", 4U) == 0 || std::strncmp(payload, "zacus:", 6U) == 0) { - copyText(last_type_, sizeof(last_type_), "app"); + core::copyText(last_type_, sizeof(last_type_), "app"); return; } - copyText(last_type_, sizeof(last_type_), "text"); + core::copyText(last_type_, sizeof(last_type_), "text"); } void updateStatusEvent() { @@ -685,7 +687,7 @@ class QrScannerModule : public ModuleBase { "scan=%u type=%s", scanning_ ? 1U : 0U, last_type_); - copyText(status_.last_event, sizeof(status_.last_event), event); + core::copyText(status_.last_event, sizeof(status_.last_event), event); } bool scanning_ = false; @@ -710,51 +712,51 @@ class DictaphoneModule : public ModuleBase { void handleAction(const AppAction& action) override { ModuleBase::handleAction(action); - if (equalsIgnoreCase(action.name, "status")) { + if (core::equalsIgnoreCase(action.name, "status")) { updateStatusEvent(); return; } - if (equalsIgnoreCase(action.name, "record_start")) { + if (core::equalsIgnoreCase(action.name, "record_start")) { const uint16_t sec = static_cast(parseUint(action.payload, 30U)); if (!context_.media->startRecording(sec, nullptr)) { setError("record_start_failed"); } else { setError(""); - copyText(status_.last_event, sizeof(status_.last_event), "recording=1"); + core::copyText(status_.last_event, sizeof(status_.last_event), "recording=1"); } return; } - if (equalsIgnoreCase(action.name, "record_stop")) { + if (core::equalsIgnoreCase(action.name, "record_stop")) { if (!context_.media->stopRecording()) { setError("record_stop_failed"); } else { setError(""); - copyText(status_.last_event, sizeof(status_.last_event), "recording=0"); + core::copyText(status_.last_event, sizeof(status_.last_event), "recording=0"); } return; } - if (equalsIgnoreCase(action.name, "play_file")) { + if (core::equalsIgnoreCase(action.name, "play_file")) { char path[120] = {0}; if (!normalizeRecordPath(action.payload, path, sizeof(path)) || !context_.media->play(path, context_.audio)) { setError("play_file_failed"); } else { setError(""); - copyText(status_.last_event, sizeof(status_.last_event), "play_ok"); + core::copyText(status_.last_event, sizeof(status_.last_event), "play_ok"); } return; } - if (equalsIgnoreCase(action.name, "delete_file")) { + if (core::equalsIgnoreCase(action.name, "delete_file")) { char path[120] = {0}; if (!normalizeRecordPath(action.payload, path, sizeof(path)) || !LittleFS.remove(path)) { setError("delete_file_failed"); } else { setError(""); - copyText(status_.last_event, sizeof(status_.last_event), "delete_ok"); + core::copyText(status_.last_event, sizeof(status_.last_event), "delete_ok"); } return; } - if (equalsIgnoreCase(action.name, "list_records")) { + if (core::equalsIgnoreCase(action.name, "list_records")) { String list_json; if (!context_.media->listFiles("records", &list_json)) { setError("list_records_failed"); @@ -766,7 +768,7 @@ class DictaphoneModule : public ModuleBase { } char event[40] = {0}; std::snprintf(event, sizeof(event), "records=%u", static_cast(count)); - copyText(status_.last_event, sizeof(status_.last_event), event); + core::copyText(status_.last_event, sizeof(status_.last_event), event); setError(""); } return; @@ -783,7 +785,7 @@ class DictaphoneModule : public ModuleBase { private: void updateStatusEvent() { - copyText(status_.last_event, sizeof(status_.last_event), "records=ready"); + core::copyText(status_.last_event, sizeof(status_.last_event), "records=ready"); } bool normalizeRecordPath(const char* payload, char* out, size_t out_size) const { @@ -791,12 +793,12 @@ class DictaphoneModule : public ModuleBase { return false; } if (payload[0] == '/') { - copyText(out, out_size, payload); + core::copyText(out, out_size, payload); return true; } char path[120] = {0}; std::snprintf(path, sizeof(path), "/recorder/%s", payload); - copyText(out, out_size, path); + core::copyText(out, out_size, path); return true; } }; @@ -819,7 +821,7 @@ class TimerToolsModule : public ModuleBase { cd_done_notified_ = false; countdown_visual_until_ms_ = 0U; setError(""); - copyText(status_.last_event, sizeof(status_.last_event), "timer_ready"); + core::copyText(status_.last_event, sizeof(status_.last_event), "timer_ready"); return true; } @@ -852,11 +854,11 @@ class TimerToolsModule : public ModuleBase { void handleAction(const AppAction& action) override { ModuleBase::handleAction(action); const uint32_t now_ms = millis(); - if (equalsIgnoreCase(action.name, "status")) { + if (core::equalsIgnoreCase(action.name, "status")) { writeStatusEvent(); return; } - if (equalsIgnoreCase(action.name, "sw_start")) { + if (core::equalsIgnoreCase(action.name, "sw_start")) { if (!sw_running_) { sw_running_ = true; sw_started_ms_ = now_ms; @@ -864,7 +866,7 @@ class TimerToolsModule : public ModuleBase { setError(""); return; } - if (equalsIgnoreCase(action.name, "sw_stop")) { + if (core::equalsIgnoreCase(action.name, "sw_stop")) { if (sw_running_) { sw_acc_ms_ += (now_ms - sw_started_ms_); sw_running_ = false; @@ -872,12 +874,12 @@ class TimerToolsModule : public ModuleBase { setError(""); return; } - if (equalsIgnoreCase(action.name, "sw_lap")) { + if (core::equalsIgnoreCase(action.name, "sw_lap")) { sw_lap_ms_ = sw_running_ ? (sw_acc_ms_ + (now_ms - sw_started_ms_)) : sw_acc_ms_; setError(""); return; } - if (equalsIgnoreCase(action.name, "sw_reset")) { + if (core::equalsIgnoreCase(action.name, "sw_reset")) { sw_running_ = false; sw_started_ms_ = 0U; sw_acc_ms_ = 0U; @@ -886,7 +888,7 @@ class TimerToolsModule : public ModuleBase { setError(""); return; } - if (equalsIgnoreCase(action.name, "cd_set")) { + if (core::equalsIgnoreCase(action.name, "cd_set")) { DynamicJsonDocument body(192); uint32_t seconds = 0U; if (parseJsonPayload(action, &body) && body["seconds"].is()) { @@ -901,7 +903,7 @@ class TimerToolsModule : public ModuleBase { setError(""); return; } - if (equalsIgnoreCase(action.name, "cd_start")) { + if (core::equalsIgnoreCase(action.name, "cd_start")) { if (cd_duration_ms_ == 0U) { cd_duration_ms_ = 60000U; } @@ -911,7 +913,7 @@ class TimerToolsModule : public ModuleBase { setError(""); return; } - if (equalsIgnoreCase(action.name, "cd_pause")) { + if (core::equalsIgnoreCase(action.name, "cd_pause")) { if (cd_running_) { const uint32_t elapsed = now_ms - cd_started_ms_; cd_duration_ms_ = (elapsed >= cd_duration_ms_) ? 0U : (cd_duration_ms_ - elapsed); @@ -921,7 +923,7 @@ class TimerToolsModule : public ModuleBase { setError(""); return; } - if (equalsIgnoreCase(action.name, "cd_reset")) { + if (core::equalsIgnoreCase(action.name, "cd_reset")) { cd_running_ = false; cd_duration_ms_ = 0U; cd_remaining_ms_ = 0U; @@ -934,7 +936,7 @@ class TimerToolsModule : public ModuleBase { private: void notifyCountdownDone(uint32_t now_ms) { - copyText(status_.last_event, sizeof(status_.last_event), "countdown_done"); + core::copyText(status_.last_event, sizeof(status_.last_event), "countdown_done"); setError(""); if (context_.storage != nullptr && context_.audio != nullptr && context_.media != nullptr) { static constexpr const char* kDoneCandidates[] = { @@ -964,7 +966,7 @@ class TimerToolsModule : public ModuleBase { static_cast(sw_elapsed_ms_), static_cast(cd_remaining_ms_), cd_running_ ? 1U : 0U); - copyText(status_.last_event, sizeof(status_.last_event), event); + core::copyText(status_.last_event, sizeof(status_.last_event), event); setError(""); } @@ -998,17 +1000,17 @@ class FlashlightModule : public ModuleBase { is_on_ = false; level_ = 96U; setError(""); - copyText(status_.last_event, sizeof(status_.last_event), "light_ready"); + core::copyText(status_.last_event, sizeof(status_.last_event), "light_ready"); return true; } void handleAction(const AppAction& action) override { ModuleBase::handleAction(action); - if (equalsIgnoreCase(action.name, "status")) { + if (core::equalsIgnoreCase(action.name, "status")) { updateStatusEvent(); return; } - if (equalsIgnoreCase(action.name, "light_on")) { + if (core::equalsIgnoreCase(action.name, "light_on")) { if (!applyLight(true)) { setError("light_on_failed"); } else { @@ -1016,14 +1018,14 @@ class FlashlightModule : public ModuleBase { } return; } - if (equalsIgnoreCase(action.name, "light_off")) { + if (core::equalsIgnoreCase(action.name, "light_off")) { context_.hardware->clearManualLed(); is_on_ = false; setError(""); updateStatusEvent(); return; } - if (equalsIgnoreCase(action.name, "light_toggle")) { + if (core::equalsIgnoreCase(action.name, "light_toggle")) { if (!applyLight(!is_on_)) { setError("light_toggle_failed"); } else { @@ -1031,7 +1033,7 @@ class FlashlightModule : public ModuleBase { } return; } - if (equalsIgnoreCase(action.name, "set_level")) { + if (core::equalsIgnoreCase(action.name, "set_level")) { DynamicJsonDocument body(128); uint32_t parsed = level_; if (parseJsonPayload(action, &body) && body["level"].is()) { @@ -1087,7 +1089,7 @@ class FlashlightModule : public ModuleBase { "on=%u level=%u", is_on_ ? 1U : 0U, static_cast(level_)); - copyText(status_.last_event, sizeof(status_.last_event), event); + core::copyText(status_.last_event, sizeof(status_.last_event), event); } uint8_t level_ = 120U; @@ -1206,17 +1208,17 @@ class CalculatorModule : public ModuleBase { public: void handleAction(const AppAction& action) override { ModuleBase::handleAction(action); - if (equalsIgnoreCase(action.name, "status")) { + if (core::equalsIgnoreCase(action.name, "status")) { writeStatusEvent(); return; } - if (equalsIgnoreCase(action.name, "clear")) { + if (core::equalsIgnoreCase(action.name, "clear")) { result_ = 0.0; setError(""); writeStatusEvent(); return; } - if (!equalsIgnoreCase(action.name, "eval")) { + if (!core::equalsIgnoreCase(action.name, "eval")) { setError("unsupported_action"); return; } @@ -1231,7 +1233,7 @@ class CalculatorModule : public ModuleBase { setError("eval_error"); char msg[40] = {0}; std::snprintf(msg, sizeof(msg), "eval_error@%d", error); - copyText(status_.last_event, sizeof(status_.last_event), msg); + core::copyText(status_.last_event, sizeof(status_.last_event), msg); return; } result_ = value; @@ -1252,7 +1254,7 @@ class CalculatorModule : public ModuleBase { void writeStatusEvent() { char msg[40] = {0}; std::snprintf(msg, sizeof(msg), "result=%.4f", result_); - copyText(status_.last_event, sizeof(status_.last_event), msg); + core::copyText(status_.last_event, sizeof(status_.last_event), msg); } double result_ = 0.0; @@ -1282,14 +1284,14 @@ class KidsCreativeModule : public ModuleBase { ModuleBase::handleAction(action); DynamicJsonDocument body(384); (void)parseJsonPayload(action, &body); - if (equalsIgnoreCase(action.name, "open_canvas")) { + if (core::equalsIgnoreCase(action.name, "open_canvas")) { if (body["color"].is()) { current_color_ = body["color"].as(); } setError(""); return; } - if (equalsIgnoreCase(action.name, "stroke")) { + if (core::equalsIgnoreCase(action.name, "stroke")) { ++stroke_count_; if (body["color"].is()) { current_color_ = body["color"].as(); @@ -1298,7 +1300,7 @@ class KidsCreativeModule : public ModuleBase { } return; } - if (equalsIgnoreCase(action.name, "fill")) { + if (core::equalsIgnoreCase(action.name, "fill")) { if (body["color"].is()) { current_color_ = body["color"].as(); } else if (action.payload[0] != '\0') { @@ -1306,18 +1308,18 @@ class KidsCreativeModule : public ModuleBase { } return; } - if (equalsIgnoreCase(action.name, "undo")) { + if (core::equalsIgnoreCase(action.name, "undo")) { if (stroke_count_ > 0U) { --stroke_count_; } return; } - if (equalsIgnoreCase(action.name, "clear") || equalsIgnoreCase(action.name, "clear_canvas")) { + if (core::equalsIgnoreCase(action.name, "clear") || core::equalsIgnoreCase(action.name, "clear_canvas")) { stroke_count_ = 0U; current_color_ = "#000000"; return; } - if (equalsIgnoreCase(action.name, "save")) { + if (core::equalsIgnoreCase(action.name, "save")) { String target = (action.payload[0] != '\0' && action.payload[0] != '{') ? String(action.payload) : String(); if (body["path"].is()) { target = body["path"].as(); @@ -1330,7 +1332,7 @@ class KidsCreativeModule : public ModuleBase { } return; } - if (equalsIgnoreCase(action.name, "load")) { + if (core::equalsIgnoreCase(action.name, "load")) { String source = (action.payload[0] != '\0' && action.payload[0] != '{') ? String(action.payload) : String(); if (body["path"].is()) { source = body["path"].as(); @@ -1401,7 +1403,7 @@ class KidsLearningModule : public ModuleBase { ModuleBase::handleAction(action); DynamicJsonDocument body(512); (void)parseJsonPayload(action, &body); - if (equalsIgnoreCase(action.name, "lesson_open")) { + if (core::equalsIgnoreCase(action.name, "lesson_open")) { if (body["lesson"].is()) { lesson_id_ = body["lesson"].as(); } else if (action.payload[0] != '\0' && action.payload[0] != '{') { @@ -1411,19 +1413,19 @@ class KidsLearningModule : public ModuleBase { saveProgress(); return; } - if (equalsIgnoreCase(action.name, "lesson_next")) { + if (core::equalsIgnoreCase(action.name, "lesson_next")) { lesson_step_ += 1U; saveProgress(); return; } - if (equalsIgnoreCase(action.name, "lesson_prev")) { + if (core::equalsIgnoreCase(action.name, "lesson_prev")) { if (lesson_step_ > 0U) { lesson_step_ -= 1U; } saveProgress(); return; } - if (equalsIgnoreCase(action.name, "quiz_answer")) { + if (core::equalsIgnoreCase(action.name, "quiz_answer")) { String answer; if (body["answer"].is()) { answer = body["answer"].as(); @@ -1437,18 +1439,18 @@ class KidsLearningModule : public ModuleBase { saveProgress(); return; } - if (equalsIgnoreCase(action.name, "session_start") || equalsIgnoreCase(action.name, "play")) { + if (core::equalsIgnoreCase(action.name, "session_start") || core::equalsIgnoreCase(action.name, "play")) { startGuidedAudio(action.payload, body); return; } - if (equalsIgnoreCase(action.name, "pause")) { + if (core::equalsIgnoreCase(action.name, "pause")) { if (context_.audio != nullptr) { context_.audio->stop(); } saveProgress(); return; } - if (equalsIgnoreCase(action.name, "session_stop") || equalsIgnoreCase(action.name, "stop")) { + if (core::equalsIgnoreCase(action.name, "session_stop") || core::equalsIgnoreCase(action.name, "stop")) { if (context_.media != nullptr) { context_.media->stop(context_.audio); } else if (context_.audio != nullptr) { @@ -1457,7 +1459,7 @@ class KidsLearningModule : public ModuleBase { saveProgress(); return; } - if (equalsIgnoreCase(action.name, "seek_ms")) { + if (core::equalsIgnoreCase(action.name, "seek_ms")) { cursor_ms_ = parseUint(action.payload, cursor_ms_); saveProgress(); return; @@ -1483,7 +1485,7 @@ class KidsLearningModule : public ModuleBase { } else { target = String("/apps/") + app_id_ + "/audio/session.mp3"; if (!LittleFS.exists(target.c_str())) { - target = "/music/boot_radio.mp3"; + target = defaultBundledAudioForApp(app_id_.c_str()); } } bool ok = false; @@ -1495,7 +1497,7 @@ class KidsLearningModule : public ModuleBase { } } if (startsWithIgnoreCase(target.c_str(), "http://") || startsWithIgnoreCase(target.c_str(), "https://")) { - ok = context_.audio->playUrl(target.c_str()); + ok = context_.audio->play(target.c_str()); } else if (context_.storage != nullptr && context_.storage->fileExists(target.c_str()) && context_.media != nullptr) { ok = context_.media->play(target.c_str(), context_.audio); } @@ -1596,13 +1598,13 @@ class NesEmulatorModule : public ModuleBase { sizeof(event), "fps=%lu", static_cast(target_fps_)); - copyText(status_.last_event, sizeof(status_.last_event), event); + core::copyText(status_.last_event, sizeof(status_.last_event), event); } } void handleAction(const AppAction& action) override { ModuleBase::handleAction(action); - if (equalsIgnoreCase(action.name, "list_roms")) { + if (core::equalsIgnoreCase(action.name, "list_roms")) { uint32_t count = 0U; File dir = LittleFS.open("/apps/nes_emulator/roms", "r"); if (dir && dir.isDirectory()) { @@ -1619,7 +1621,7 @@ class NesEmulatorModule : public ModuleBase { } char event[40] = {0}; std::snprintf(event, sizeof(event), "roms=%lu", static_cast(count)); - copyText(status_.last_event, sizeof(status_.last_event), event); + core::copyText(status_.last_event, sizeof(status_.last_event), event); return; } @@ -1630,11 +1632,11 @@ class NesEmulatorModule : public ModuleBase { rom_path = body["path"].as(); } - if (equalsIgnoreCase(action.name, "rom_validate")) { + if (core::equalsIgnoreCase(action.name, "rom_validate")) { validateRom(rom_path.c_str()); return; } - if (equalsIgnoreCase(action.name, "rom_start")) { + if (core::equalsIgnoreCase(action.name, "rom_start")) { if (!validateRom(rom_path.c_str())) { return; } @@ -1652,48 +1654,48 @@ class NesEmulatorModule : public ModuleBase { next_frame_ms_ = millis(); frame_count_ = 0U; setError(""); - copyText(status_.last_event, sizeof(status_.last_event), "rom_start"); + core::copyText(status_.last_event, sizeof(status_.last_event), "rom_start"); return; } - if (equalsIgnoreCase(action.name, "rom_stop")) { + if (core::equalsIgnoreCase(action.name, "rom_stop")) { emu_running_ = false; rom_buffer_.clear(); rom_buffer_.shrink_to_fit(); setError(""); - copyText(status_.last_event, sizeof(status_.last_event), "rom_stop"); + core::copyText(status_.last_event, sizeof(status_.last_event), "rom_stop"); return; } - if (equalsIgnoreCase(action.name, "set_fps")) { + if (core::equalsIgnoreCase(action.name, "set_fps")) { const uint32_t requested = parseUint(action.payload, target_fps_); target_fps_ = (requested < 30U) ? 30U : (requested > 60U ? 60U : requested); return; } - if (equalsIgnoreCase(action.name, "input") || - equalsIgnoreCase(action.name, "btn_down") || - equalsIgnoreCase(action.name, "btn_up")) { + if (core::equalsIgnoreCase(action.name, "input") || + core::equalsIgnoreCase(action.name, "btn_down") || + core::equalsIgnoreCase(action.name, "btn_up")) { uint16_t mask = input_mask_; if (has_json && body["mask"].is()) { mask = body["mask"].as(); } else { mask = static_cast(parseUint(action.payload, input_mask_)); } - if (equalsIgnoreCase(action.name, "btn_down")) { + if (core::equalsIgnoreCase(action.name, "btn_down")) { input_mask_ |= mask; - } else if (equalsIgnoreCase(action.name, "btn_up")) { + } else if (core::equalsIgnoreCase(action.name, "btn_up")) { input_mask_ &= static_cast(~mask); } else { input_mask_ = mask; } return; } - if (equalsIgnoreCase(action.name, "core_status")) { + if (core::equalsIgnoreCase(action.name, "core_status")) { char event[56] = {0}; std::snprintf(event, sizeof(event), "run=%u frames=%lu", emu_running_ ? 1U : 0U, static_cast(frame_count_)); - copyText(status_.last_event, sizeof(status_.last_event), event); + core::copyText(status_.last_event, sizeof(status_.last_event), event); return; } } @@ -1756,7 +1758,7 @@ class NesEmulatorModule : public ModuleBase { return false; } setError(""); - copyText(status_.last_event, sizeof(status_.last_event), "rom_ok"); + core::copyText(status_.last_event, sizeof(status_.last_event), "rom_ok"); return true; } @@ -1821,46 +1823,46 @@ class NesEmulatorModule : public ModuleBase { } // namespace std::unique_ptr createAppModule(const AppDescriptor& descriptor) { - if (equalsIgnoreCase(descriptor.id, "audio_player") || - equalsIgnoreCase(descriptor.id, "kids_webradio") || - equalsIgnoreCase(descriptor.id, "kids_podcast") || - equalsIgnoreCase(descriptor.id, "kids_music")) { + if (core::equalsIgnoreCase(descriptor.id, "audio_player") || + core::equalsIgnoreCase(descriptor.id, "kids_webradio") || + core::equalsIgnoreCase(descriptor.id, "kids_podcast") || + core::equalsIgnoreCase(descriptor.id, "kids_music")) { return std::unique_ptr(new AudioPlayerModule()); } - if (equalsIgnoreCase(descriptor.id, "audiobook_player")) { + if (core::equalsIgnoreCase(descriptor.id, "audiobook_player")) { return std::unique_ptr(new AudiobookModule()); } - if (equalsIgnoreCase(descriptor.id, "camera_video")) { + if (core::equalsIgnoreCase(descriptor.id, "camera_video")) { return std::unique_ptr(new CameraVideoModule()); } - if (equalsIgnoreCase(descriptor.id, "qr_scanner")) { + if (core::equalsIgnoreCase(descriptor.id, "qr_scanner")) { return std::unique_ptr(new QrScannerModule()); } - if (equalsIgnoreCase(descriptor.id, "dictaphone")) { + if (core::equalsIgnoreCase(descriptor.id, "dictaphone")) { return std::unique_ptr(new DictaphoneModule()); } - if (equalsIgnoreCase(descriptor.id, "timer_tools")) { + if (core::equalsIgnoreCase(descriptor.id, "timer_tools")) { return std::unique_ptr(new TimerToolsModule()); } - if (equalsIgnoreCase(descriptor.id, "flashlight")) { + if (core::equalsIgnoreCase(descriptor.id, "flashlight")) { return std::unique_ptr(new FlashlightModule()); } - if (equalsIgnoreCase(descriptor.id, "calculator")) { + if (core::equalsIgnoreCase(descriptor.id, "calculator")) { return std::unique_ptr(new CalculatorModule()); } - if (equalsIgnoreCase(descriptor.id, "kids_drawing") || - equalsIgnoreCase(descriptor.id, "kids_coloring")) { + if (core::equalsIgnoreCase(descriptor.id, "kids_drawing") || + core::equalsIgnoreCase(descriptor.id, "kids_coloring")) { return std::unique_ptr(new KidsCreativeModule()); } - if (equalsIgnoreCase(descriptor.id, "kids_yoga") || - equalsIgnoreCase(descriptor.id, "kids_meditation") || - equalsIgnoreCase(descriptor.id, "kids_languages") || - equalsIgnoreCase(descriptor.id, "kids_math") || - equalsIgnoreCase(descriptor.id, "kids_science") || - equalsIgnoreCase(descriptor.id, "kids_geography")) { + if (core::equalsIgnoreCase(descriptor.id, "kids_yoga") || + core::equalsIgnoreCase(descriptor.id, "kids_meditation") || + core::equalsIgnoreCase(descriptor.id, "kids_languages") || + core::equalsIgnoreCase(descriptor.id, "kids_math") || + core::equalsIgnoreCase(descriptor.id, "kids_science") || + core::equalsIgnoreCase(descriptor.id, "kids_geography")) { return std::unique_ptr(new KidsLearningModule()); } - if (equalsIgnoreCase(descriptor.id, "nes_emulator")) { + if (core::equalsIgnoreCase(descriptor.id, "nes_emulator")) { return std::unique_ptr(new NesEmulatorModule()); } return nullptr; diff --git a/ui_freenove_allinone/src/app/modules/audio_player_module.cpp b/ui_freenove_allinone/src/app/modules/audio_player_module.cpp new file mode 100644 index 0000000..f45cc9a --- /dev/null +++ b/ui_freenove_allinone/src/app/modules/audio_player_module.cpp @@ -0,0 +1,66 @@ +// audio_player_module.cpp - Audio player module bridging to AmigaAudioPlayer. +#include "app/modules/audio_player_module.h" + +#include + +#include "app/app_registry.h" +#include "runtime/runtime_services.h" + +bool AudioPlayerModule::onOpen(const AppEntry& entry, const char* mode, RuntimeServices* services) { + (void)entry; + (void)mode; + if (services == nullptr || services->audio == nullptr) { + std::strncpy(last_error_, "no_audio_service", sizeof(last_error_) - 1U); + state_ = AppModuleState::kError; + return false; + } + state_ = AppModuleState::kRunning; + last_error_[0] = '\0'; + Serial.println("[APP_MOD] audio_player open"); + return true; +} + +void AudioPlayerModule::onClose(const char* reason, RuntimeServices* services) { + (void)reason; + if (services != nullptr && services->audio != nullptr) { + services->audio->stop(); + } + state_ = AppModuleState::kIdle; + Serial.printf("[APP_MOD] audio_player close reason=%s\n", reason != nullptr ? reason : "n/a"); +} + +void AudioPlayerModule::onTick(uint32_t now_ms, RuntimeServices* services) { + (void)now_ms; + (void)services; + // AmigaAudioPlayer is ticked separately from main.cpp for now. +} + +bool AudioPlayerModule::onAction(const char* action, const char* payload, RuntimeServices* services) { + if (action == nullptr || services == nullptr || services->audio == nullptr) { + return false; + } + + if (std::strcmp(action, "play") == 0) { + return services->audio->play(payload); + } + if (std::strcmp(action, "stop") == 0) { + services->audio->stop(); + return true; + } + if (std::strcmp(action, "pause") == 0) { + services->audio->stop(); + return true; + } + if (std::strcmp(action, "set_volume") == 0 && payload != nullptr) { + const int vol = atoi(payload); + if (vol >= 0 && vol <= 100) { + // Map 0-100 to 0-21. + const uint8_t mapped = static_cast((vol * 21U) / 100U); + services->audio->setVolume(mapped); + return true; + } + } + + Serial.printf("[APP_MOD] audio_player unknown action=%s\n", action); + return false; +} diff --git a/ui_freenove_allinone/src/app/runtime_web_service 2.cpp b/ui_freenove_allinone/src/app/runtime_web_service 2.cpp deleted file mode 100644 index 87dcb13..0000000 --- a/ui_freenove_allinone/src/app/runtime_web_service 2.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include "app/runtime_web_service.h" - -void RuntimeWebService::configure(SetupWebUiFn setup_web_ui) { - setup_web_ui_ = setup_web_ui; -} - -void RuntimeWebService::setupWebUi() const { - if (setup_web_ui_ == nullptr) { - return; - } - setup_web_ui_(); -} diff --git a/ui_freenove_allinone/src/audio/audio_manager.cpp b/ui_freenove_allinone/src/audio/audio_manager.cpp index 302d6ea..cb53d5f 100644 --- a/ui_freenove_allinone/src/audio/audio_manager.cpp +++ b/ui_freenove_allinone/src/audio/audio_manager.cpp @@ -22,11 +22,19 @@ #include #include #include +#if __has_include() +#include +#define ZACUS_HAS_AUDIO_HEAP_CAPS 1 +#else +#define ZACUS_HAS_AUDIO_HEAP_CAPS 0 +#endif +#else +#define ZACUS_HAS_AUDIO_HEAP_CAPS 0 #endif namespace { -constexpr char kDiagnosticTrackPath[] = "/music/boot_radio.mp3"; +constexpr char kDiagnosticTrackPath[] = "/apps/audio_player/audio/default.mp3"; constexpr uint8_t kMaxTrackPathLen = 120U; constexpr uint16_t kBitrateScanBytes = 4096U; constexpr uint8_t kAudioDoneQueueDepth = 6U; @@ -39,6 +47,8 @@ constexpr uint16_t kAudioPumpIdleDelayMs = 4U; constexpr uint16_t kAudioStateLockTimeoutMs = 20U; constexpr uint32_t kAudioUnderrunThresholdBytes = 768U; constexpr uint32_t kAudioUnderrunCooldownMs = 250U; +constexpr size_t kAudioMinLargestDmaBlockBytes = 20000U; +constexpr uint32_t kAudioRestoreRetryDelayMs = 2500U; struct AudioPinProfile { int bck; @@ -112,6 +122,28 @@ bool resolveFileSystem(bool use_sd, fs::FS*& out_fs, const char*& out_label) { #endif } +bool hasAudioDmaBudget(size_t* out_largest_dma, size_t* out_free_dma) { +#if ZACUS_HAS_AUDIO_HEAP_CAPS + const size_t largest_dma = heap_caps_get_largest_free_block(MALLOC_CAP_DMA); + const size_t free_dma = heap_caps_get_free_size(MALLOC_CAP_DMA); + if (out_largest_dma != nullptr) { + *out_largest_dma = largest_dma; + } + if (out_free_dma != nullptr) { + *out_free_dma = free_dma; + } + return largest_dma >= kAudioMinLargestDmaBlockBytes; +#else + if (out_largest_dma != nullptr) { + *out_largest_dma = 0U; + } + if (out_free_dma != nullptr) { + *out_free_dma = 0U; + } + return true; +#endif +} + uint16_t parseMp3BitrateHeader(const uint8_t* header, size_t header_len) { if (header == nullptr || header_len < 4U) { return 0U; @@ -416,6 +448,15 @@ bool AudioManager::ensurePlayer() { Serial.println("[AUDIO] player disabled: PSRAM not detected"); return false; } + size_t largest_dma = 0U; + size_t free_dma = 0U; + if (!hasAudioDmaBudget(&largest_dma, &free_dma)) { + Serial.printf("[AUDIO] player deferred: largest_dma=%lu free_dma=%lu need>=%lu\n", + static_cast(largest_dma), + static_cast(free_dma), + static_cast(kAudioMinLargestDmaBlockBytes)); + return false; + } player_ = std::unique_ptr