diff --git a/AGENTS.md b/AGENTS.md index 287f40f..de5c967 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,8 @@ Instructions for humans and coding agents working in this repository. - Integration branch is **`develop`**. Feature and refactor work is done on a branch cut from `develop`, then opened as a PR **into `develop`**. - Do not PR this work into `main`. `main` is not the day-to-day integration line. - Active work branch for this refactor: `refactor/esp-idf-modular-ui` (rebase onto `develop` if the branch still points at `main`). -- Architecture intent: `docs/CURRENT_STATE.md`, `docs/TARGET_ARCHITECTURE.md`. Update those docs when the design changes; do not leave the tree contradicting them. +- Architecture intent: `docs/CURRENT_STATE.md`, `docs/TARGET_ARCHITECTURE.md`, `docs/CICD.md`. +- Implementation sessions: `docs/megaplans/REFACTOR-MEGAPLAN.md` then exactly one `docs/megaplans/refactor/Pxx-*.md`. Do not start the next phase in the same session. - Do not commit secrets (Git tokens, Wi-Fi passwords, `sdkconfig` with provisioned keys). ## Layout @@ -27,6 +28,7 @@ Instructions for humans and coding agents working in this repository. | `components/hal_*` | Hardware adapters | | `components/board_*` | Pin maps and board bring-up | | `docs/` | Design notes | +| `.gitea/workflows/` | Gitea Actions (tests + firmware builds) | New behaviour goes into `components/`, not into larger `menu.cpp` files. @@ -85,5 +87,7 @@ New behaviour goes into `components/`, not into larger `menu.cpp` files. ## Tests -- Prefer host-side tests for `app_process` lookup and the machine state machine with a stub motor. +- Prefer host-side tests for `app_process` lookup and the machine state machine with a stub motor (`tests/host`, CMake + CTest). - On-target smoke: select process, start a short Custom step, Esc mid-step, confirm EN high and Resume works. +- CI is Gitea Actions: host tests must pass before either firmware image is built. See `docs/CICD.md`. +- After `tests/host` exists, run it before push. After the IDF skeleton exists, a local `idf.py` build for the board you touched is expected. diff --git a/docs/CICD.md b/docs/CICD.md new file mode 100644 index 0000000..a14cedc --- /dev/null +++ b/docs/CICD.md @@ -0,0 +1,243 @@ +# CI/CD — Gitea Actions + +Status: planned on `refactor/esp-idf-modular-ui`. Implementation follows the IDF skeleton +(migration step 2 in `docs/TARGET_ARCHITECTURE.md`). Workflows live in **`.gitea/workflows/`** +(Gitea Actions, GitHub-compatible YAML). PRs target **`develop`**. + +## Purpose + +Every change that lands on `develop` must: + +1. Run **host unit tests** for recipe tables and the machine state machine (no hardware). +2. **Build two firmware images**: `board_wroom` (ESP32 classic) and `board_jc4827w543` (ESP32-S3). +3. Publish flashable artifacts (app binary, bootloader, partition table, merged image, size report). + +Legacy PlatformIO (`platformio.ini`) is not a CI target. + +## Runner requirements + +Gitea `act_runner` must be registered against `git.i3omb.com` with labels that match `runs-on`. + +| Workflow `runs-on` | Typical runner label | Notes | +| --- | --- | --- | +| `ubuntu-latest` | `ubuntu-latest:docker://gitea/runner-images:ubuntu-latest` | Host tests, checkout, artifact upload | +| (job `container`) | Docker socket available on the runner | Firmware jobs use `espressif/idf:` as the job container | + +Firmware builds need: + +- Docker on the runner (job-level `container:`) +- Enough disk for the IDF image (~several GB) and two `build/` trees +- Outbound pull of `docker.io/espressif/idf` (or a mirror on the Gitea registry) + +If the instance cannot pull Docker Hub, mirror `espressif/idf:v5.4` into the Gitea container registry and point `container.image` at that. + +Do not depend on `espressif/esp-idf-ci-action` or other GitHub-hosted actions for the compile step. Those often assume GitHub path layout. Drive `idf.py` inside `espressif/idf` instead. `actions/checkout` and `actions/upload-artifact` (or Gitea’s equivalents) are fine. + +## Triggers + +| Event | What runs | +| --- | --- | +| `pull_request` into `develop` | tests + both firmware builds (required check) | +| `push` to `develop` | tests + both firmware builds; keep artifacts | +| `push` to `refactor/**` and `feature/**` | same, so the branch is green before the PR | +| `push` tags `v*` | tests + builds + attach binaries to the Gitea release | +| `workflow_dispatch` | manual rebuild | + +`main` is not a CI integration branch for this refactor. + +## Jobs + +``` +test (host) ──► firmware [wroom / jc4827w543] ──► (tag only) release +``` + +Firmware must `needs: test`. A failing recipe or state-machine test must not produce a binary. + +### 1. `test` — host unit tests + +Runs on `ubuntu-latest` **without** the IDF container so the gate is fast. + +- Toolchain: CMake + GCC (C11), CTest. +- Tree: `tests/host/` linking `components/app_process` and `components/app_machine` against **stub HAL** (`tests/host/stubs/`: motor, temp, audio, display no-ops; queues replaced with a tiny fake or a vendored FreeRTOS POSIX port if the machine task is exercised). +- Framework: Unity (can be vendored) or plain `assert` + CTest. Prefer Unity so on-target tests later share the same style. +- Must cover: + - `find` / lookup of all six process names + - frozen default times, temps, CW/CCW for every step (values from `docs/CURRENT_STATE.md`) + - session ±5 s overlay does not mutate the const table + - state machine: Idle → Select → Arm → Start → Stop (EN disabled) → Resume → Complete → Return + - `Stop` is accepted in `StepRunning` and during a complete-alarm +- Recipe-value tests fail the build if defaults change without an explicit doc update. + +Command sketch: + +```bash +cmake -S tests/host -B build/host +cmake --build build/host +ctest --test-dir build/host --output-on-failure +``` + +Until `tests/host` exists, the workflow file is not merged as a required check — or the job is `if`’d on path existence. Do not ship a red pipeline on docs-only commits after the workflow is enabled; use: + +```yaml +- name: Host tests + if: hashFiles('tests/host/CMakeLists.txt') != '' +``` + +### 2. `firmware` — matrix build + +Job container: `espressif/idf:v5.4` (pin the tag; bump in one place when IDF is upgraded). + +Matrix: + +| Name | `IDF_TARGET` | Board cmake/sdkconfig | +| --- | --- | --- | +| `wroom` | `esp32` | `-DAUTOFILM_BOARD=wroom` + `sdkconfig.defaults` + `sdkconfig.wroom` | +| `jc4827w543` | `esp32s3` | `-DAUTOFILM_BOARD=jc4827w543` + `sdkconfig.defaults` + `sdkconfig.s3` | + +Steps: + +1. Checkout (full history not required). +2. `source /opt/esp/idf/export.sh` (already true in the official image entrypoint; use `idf.py` directly). +3. `idf.py set-target ${IDF_TARGET}` then + `idf.py @sdkconfig.defaults @sdkconfig. -DAUTOFILM_BOARD= build` +4. `idf.py size` (fail the job only if the binary will not fit flash; log the report either way). +5. Merge a single flash image: + `esptool.py --chip ${IDF_TARGET} merge_bin -o autofilm-${board}-${GIT_SHA}.bin @build/flash_args` +6. Upload artifacts (see below). + +S3 build must enable octal PSRAM in `sdkconfig.s3`. WROOM must not. + +Arduino-as-component is pulled by the IDF CMake on the WROOM image first; the S3 image may share it until debt burn-down. Cache `managed_components/` by lock hash when a `idf_component.yml` exists. + +### 3. `release` (tags only) + +`needs: [test, firmware]`. Download both artifact sets and attach to the Gitea release for tag `v*`: + +- `autofilm-wroom-.bin` (merged) +- `autofilm-jc4827w543-.bin` (merged) +- raw `app.bin` / `bootloader.bin` / `partition-table.bin` per board +- `sdkconfig` snapshot per board +- `size.txt` per board + +Version string is the git tag. CI also stamps `version` in firmware via `-DAUTOFILM_GIT_DESC=$(git describe --always --dirty)`. + +## Artifacts + +| File | Why | +| --- | --- | +| `autofilm--.bin` | Merged image for `esptool write_flash 0x0` | +| `build/autofilm.bin` (app) | OTA / partial flash | +| `build/bootloader/bootloader.bin` | First-time flash | +| `build/partition_table/partition-table.bin` | First-time flash | +| `build/flasher_args.json` | Exact offsets | +| `size.txt` / `size.json` | Flash/RAM budget | +| `sdkconfig` | Repro the build flags | + +Retention: 14 days on branch builds, keep release attachments. Artifact names must include board + short SHA so WROOM and S3 cannot overwrite each other. + +## Example workflow (target shape) + +Path: `.gitea/workflows/ci.yml` + +```yaml +name: ci + +on: + pull_request: + branches: [develop] + push: + branches: [develop, "refactor/**", "feature/**"] + tags: ["v*"] + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Host unit tests + if: hashFiles('tests/host/CMakeLists.txt') != '' + run: | + cmake -S tests/host -B build/host + cmake --build build/host + ctest --test-dir build/host --output-on-failure + + firmware: + needs: test + runs-on: ubuntu-latest + container: + image: docker.io/espressif/idf:v5.4 + strategy: + fail-fast: false + matrix: + include: + - board: wroom + target: esp32 + sdkconfig: sdkconfig.wroom + - board: jc4827w543 + target: esp32s3 + sdkconfig: sdkconfig.s3 + steps: + - uses: actions/checkout@v4 + - name: Build + if: hashFiles('CMakeLists.txt') != '' + run: | + git config --global --add safe.directory '*' + idf.py -B build/${{ matrix.board }} set-target ${{ matrix.target }} + idf.py -B build/${{ matrix.board }} \ + -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;${{ matrix.sdkconfig }}" \ + -D AUTOFILM_BOARD=${{ matrix.board }} \ + build + idf.py -B build/${{ matrix.board }} size | tee size-${{ matrix.board }}.txt + python $IDF_PATH/components/esptool_py/esptool/esptool.py \ + --chip ${{ matrix.target }} merge_bin \ + -o autofilm-${{ matrix.board }}-${{ gitea.sha }}.bin \ + @build/${{ matrix.board }}/flash_args + - uses: actions/upload-artifact@v3 + if: hashFiles('CMakeLists.txt') != '' + with: + name: firmware-${{ matrix.board }} + path: | + autofilm-${{ matrix.board }}-*.bin + build/${{ matrix.board }}/*.bin + build/${{ matrix.board }}/bootloader/bootloader.bin + build/${{ matrix.board }}/partition_table/partition-table.bin + build/${{ matrix.board }}/flasher_args.json + build/${{ matrix.board }}/sdkconfig + size-${{ matrix.board }}.txt +``` + +Context substitutions: Gitea accepts both `github.*` and `gitea.*` in many versions; prefer `gitea.sha` / `gitea.ref` and fall back if the runner is older. + +`hashFiles` guards keep docs-only stages of this branch from going red before the IDF tree exists. + +## Required status on PRs + +Once the IDF skeleton compiles, mark on the `develop` branch protection (Gitea repo settings): + +- `test` +- `firmware (wroom)` +- `firmware (jc4827w543)` + +No merge to `develop` with a red firmware matrix. + +## Caching (optional, second pass) + +- Cache `~/.cache/pip` and IDF tools if not using the pre-baked `espressif/idf` image. +- Cache `managed_components/` keyed on `idf_component.yml` + `idf.lock`. +- Do not cache `build/` across boards or targets. + +## Secrets + +CI must not need device tokens or Wi-Fi credentials. Firmware builds are offline-configurable. If a later job publishes to a hardware farm, use Gitea secrets — never commit them. + +## Local equivalent + +```bash +cmake -S tests/host -B build/host && cmake --build build/host && ctest --test-dir build/host --output-on-failure + +docker run --rm -v "$PWD":/project -w /project espressif/idf:v5.4 \ + bash -lc 'idf.py -B build/wroom set-target esp32 && idf.py -B build/wroom -D AUTOFILM_BOARD=wroom build' +``` + +Agents must run the host tests before pushing once `tests/host` exists. diff --git a/docs/TARGET_ARCHITECTURE.md b/docs/TARGET_ARCHITECTURE.md index 43ee752..c311fe8 100644 --- a/docs/TARGET_ARCHITECTURE.md +++ b/docs/TARGET_ARCHITECTURE.md @@ -41,6 +41,8 @@ idf.py -D AUTOFILM_BOARD=wroom @sdkconfig.defaults @sdkconfig.wroom idf.py -D AUTOFILM_BOARD=jc4827w543 @sdkconfig.defaults @sdkconfig.s3 ``` +Automated by Gitea Actions — see `docs/CICD.md`. Both images are required checks on PRs into `develop`. + WROOM remains the machine that already exists. S3 firmware is the future operator panel; mechatronics may stay on the WROOM or move when the S3 I/O map is proven. The **same** `app_machine` binary interface is compiled for both. Vendor references for the S3 panel (not copied into this repo): [lsdlsd88/JC4827W543](https://github.com/lsdlsd88/JC4827W543), [profi-max board notes](https://github.com/profi-max/JC4827W543_4.3inch_ESP32S3_board) — NV3041A + GT911, 480×272. @@ -284,13 +286,16 @@ Session overlay: `int16_t time_delta_s[step]` in RAM (L/R ±5). Not persisted. N ## Migration plan -1. **Docs + conventions** (this branch, this commit). -2. **IDF skeleton** with Arduino component, two sdkconfigs, both boards compile a blink/splash. -3. **`ui_cmd` + machine state machine** with a fake motor (log + delay) and existing recipes copied 1:1. -4. **HAL wrap** of current AccelStepper / LCD / keypad / DS18B20 / beeper on WROOM. Feature-complete vs today **plus Stop/Resume**. -5. **Delete blocking menus** (`startingMenu` / `getEntEscInput` busy loops). -6. **S3 display+touch adapter** showing the same screens; STOP hit-test wired to `Stop`. -7. **Burn-down:** RMT stepper, IDF I²C LCD or drop 2004, IDF OneWire or RMT 1-Wire, IDF LEDC tones; remove Arduino component from the S3 image first. +Executable session phases: `docs/megaplans/REFACTOR-MEGAPLAN.md`. + +1. **Docs + conventions** (P00, this branch). +2. **IDF skeleton** (P01) Arduino-free, two sdkconfigs. +3. **`ui_cmd` + `app_process` + golden tests** (P02). +4. **`app_machine` + stub HAL** (P03). Host tests become the gate. +5. **Gitea Actions** (P04) `.gitea/workflows/ci.yml`. +6. **WROOM motion HAL** (P05) then **UI cutover** (P06) including Stop/Resume. +7. **S3 display+touch** (P07). +8. **Arduino debt burn-down** (P08). ## Acceptance checks for the first behavioural cut (WROOM) diff --git a/docs/megaplans/REFACTOR-MEGAPLAN.md b/docs/megaplans/REFACTOR-MEGAPLAN.md new file mode 100644 index 0000000..42c308e --- /dev/null +++ b/docs/megaplans/REFACTOR-MEGAPLAN.md @@ -0,0 +1,79 @@ +# REFACTOR MEGAPLAN — AutoFilm-ESP32 IDF modular UI + +Audience: coding agent. One phase = one session. Do not start the next phase in the same session. + +## Protocol (every session) + +1. `git checkout refactor/esp-idf-modular-ui && git pull --ff-only` +2. Read only: `AGENTS.md`, this file (status table), **the assigned phase file**. Open `docs/TARGET_ARCHITECTURE.md` / `docs/CURRENT_STATE.md` / `docs/CICD.md` only if the phase `READ:` list says so. +3. Execute `IN` only. Honour `OUT` and `FORBIDDEN`. +4. Run `VERIFY` exactly. Do not push if any verify item fails. +5. Commits: messages listed in the phase. Imperative. No secret tokens. +6. Push `origin refactor/esp-idf-modular-ui`. PR target is `develop`, not `main`. +7. Set phase `STATUS:` to `DONE` in the phase file **and** this table. One-line `Notes` if you diverged (API name only — do not silently change behaviour). + +If blocked: stop, commit nothing broken, write `BLOCKED:` at top of the phase file with the exact error. + +## Frozen constraints (never reinterpret) + +- Recipe **values** (times s, CW, CCW, temp min/pref/max) for C41 E6 ECN-2 B&W Custom B&WREV = `src/devSequence.cpp` / `docs/CURRENT_STATE.md`. Changing values requires explicit user order + doc update same commit. +- Stop while running: Esc `'X'` (keypad r4c4) and later touch STOP. Immediate motor **EN HIGH** (disabled). Then Resume or ReturnToStepSelect. Short beep Stop and Resume. +- UI never blocks on motor, OneWire, or `tone()`. +- No Arduino types in `ui_cmd`, `app_process`, `app_machine` public headers. +- Watchdog stays **on**. No `vTaskDelete` of long-lived workers. No `vTaskDelete` of motor task. +- Session time edits ±5 s RAM only. No NVS profiles. No Custom editor. No pumps/valves/heaters (command IDs reserved only). +- `auto_advance` default **false** (after StepComplete wait for Start). Do not restore silent auto-chain unless a later phase says so. + +## Branch + +`refactor/esp-idf-modular-ui` → PR → `develop`. Rebase onto `develop` if requested; do not retarget `main`. + +## Status + +| ID | File | Session goal | STATUS | +| --- | --- | --- | --- | +| P00 | `refactor/P00-docs-lock.md` | Architecture docs already on branch | DONE | +| P01 | `refactor/P01-idf-skeleton.md` | Dual-board IDF project compiles | TODO | +| P02 | `refactor/P02-ui-cmd-process.md` | `ui_cmd` + `app_process` + golden tests | TODO | +| P03 | `refactor/P03-machine-host.md` | `app_machine` + stub HAL + SM tests | TODO | +| P04 | `refactor/P04-gitea-ci.md` | `.gitea/workflows/ci.yml` live | TODO | +| P05 | `refactor/P05-hal-wroom-motion.md` | WROOM motor/temp/audio HAL | TODO | +| P06 | `refactor/P06-ui-wroom-cutover.md` | 2004 UI + keypad + Stop/Resume cutover | TODO | +| P07 | `refactor/P07-board-s3-ui.md` | JC4827W543 display+GT911+STOP | TODO | +| P08 | `refactor/P08-idf-debt.md` | Replace Arduino drivers (RMT etc.) | TODO | + +## Dependency + +``` +P00 → P01 → P02 → P03 → P04 + ↘ P05 → P06 → P07 → P08 +P04 may start after P01 (hashFiles guards) but must not be required-on-develop until P03 VERIFY is green. +P05 requires P03 APIs. P06 requires P05. P07 requires P06 screen contract. P08 requires P06 behaviour-identical. +``` + +## Tree (end state; create only what the current phase lists) + +``` +main/ IDF entry, spawn tasks +components/ui_cmd/ cmds/events C structs +components/app_process/ const recipes + session overlay +components/app_machine/ state machine +components/app_ui/ screens → commands +components/hal_{motor,temp,input,display,audio}/ +components/board_{wroom,jc4827w543}/ +tests/host/ CMake+CTest, stub HAL +.gitea/workflows/ci.yml +sdkconfig.defaults sdkconfig.wroom sdkconfig.s3 +src/ include/ legacy; shrink after P06 +``` + +## IDF pin + +`espressif/idf:v5.4`. Bump only in P04/CICD together. + +## Do not + +- Expand scope (LVGL, NVS profiles, pumps, heater, PlatformIO CI). +- Disable TWDT. +- Add Arduino libraries beyond AccelStepper, Keypad, LiquidCrystal_I2C, OneWire, DallasTemperature (P05–P07 only). +- Commit `sdkconfig` (generated), `build/`, tokens. diff --git a/docs/megaplans/refactor/P00-docs-lock.md b/docs/megaplans/refactor/P00-docs-lock.md new file mode 100644 index 0000000..ed21f3f --- /dev/null +++ b/docs/megaplans/refactor/P00-docs-lock.md @@ -0,0 +1,16 @@ +# P00 — Docs lock + +STATUS: DONE +SESSION: already executed (commits 9558e58, d6a3399, d8fed43) +DEPENDS: none + +## Result + +Present on `refactor/esp-idf-modular-ui`: + +- `AGENTS.md` +- `docs/CURRENT_STATE.md` +- `docs/TARGET_ARCHITECTURE.md` +- `docs/CICD.md` + +Do not rewrite those unless a later phase `Notes` a real API drift. Executing agents start at **P01**. diff --git a/docs/megaplans/refactor/P01-idf-skeleton.md b/docs/megaplans/refactor/P01-idf-skeleton.md new file mode 100644 index 0000000..2022820 --- /dev/null +++ b/docs/megaplans/refactor/P01-idf-skeleton.md @@ -0,0 +1,132 @@ +# P01 — IDF dual-board skeleton + +STATUS: TODO +DEPENDS: P00 +READ: `AGENTS.md`, `docs/megaplans/REFACTOR-MEGAPLAN.md`, this file +OUT: recipes, UI, motor, Arduino-as-component, Gitea workflow, touching `src/` behaviour +FORBIDDEN: `esp_task_wdt_deinit`, Arduino `#include`, new libraries, changing recipe files + +## IN + +Create a compileable ESP-IDF v5.4 app for two boards. App: init GPIO motor-EN as disabled (HIGH), log board name, idle loop `vTaskDelay`, TWDT on. No LCD yet. + +## FILES (create) + +``` +CMakeLists.txt +main/CMakeLists.txt +main/idf_component.yml # empty dependencies: {} (no Arduino yet) +main/main.c +components/board_wroom/CMakeLists.txt +components/board_wroom/include/board.h +components/board_wroom/board.c +components/board_jc4827w543/CMakeLists.txt +components/board_jc4827w543/include/board.h +components/board_jc4827w543/board.c +sdkconfig.defaults +sdkconfig.wroom +sdkconfig.s3 +``` + +Edit `.gitignore` append: + +``` +build/ +sdkconfig +sdkconfig.old +managed_components/ +dependencies.lock +``` + +Do not delete `platformio.ini` / `src/` / `include/`. + +## Board CMake + +Exactly one board component is compiled. + +`main/CMakeLists.txt`: + +``` +if(NOT DEFINED AUTOFILM_BOARD) + set(AUTOFILM_BOARD wroom CACHE STRING "wroom|jc4827w543") +endif() +if(AUTOFILM_BOARD STREQUAL "wroom") + set(BOARD_COMP board_wroom) +elseif(AUTOFILM_BOARD STREQUAL "jc4827w543") + set(BOARD_COMP board_jc4827w543) +else() + message(FATAL_ERROR "AUTOFILM_BOARD=${AUTOFILM_BOARD}") +endif() +idf_component_register(SRCS "main.c" INCLUDE_DIRS "." REQUIRES ${BOARD_COMP} driver) +``` + +Both `board.h` **identical API**: + +```c +#pragma once +#include "driver/gpio.h" +const char *board_name(void); +gpio_num_t board_pin_motor_en(void); /* WROOM: 27. S3: GPIO_NUM_NC until P07 maps real spare IO */ +int board_motor_en_disable_level(void); /* 1 = HIGH means disabled */ +``` + +`board_wroom`: `name="wroom"`, EN=27, disable_level=1. +`board_jc4827w543`: `name="jc4827w543"`, EN=`GPIO_NUM_NC`, disable_level=1. If EN is NC, `main.c` skips gpio config. + +## main.c contract + +- `ESP_LOGI("app", "board=%s", board_name());` +- if EN valid: `gpio_set_direction`, `gpio_set_level(pin, disable_level)` **before** any other actuator +- do **not** call `disableWatchdogTimers` or `esp_task_wdt_deinit` +- loop: `vTaskDelay(pdMS_TO_TICKS(1000))` + +## sdkconfig (minimal) + +`sdkconfig.defaults`: + +``` +CONFIG_ESP_TASK_WDT_EN=y +CONFIG_ESP_TASK_WDT_TIMEOUT_S=10 +CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=y +``` + +`sdkconfig.wroom`: + +``` +CONFIG_IDF_TARGET="esp32" +``` + +`sdkconfig.s3`: + +``` +CONFIG_IDF_TARGET="esp32s3" +CONFIG_SPIRAM=y +CONFIG_SPIRAM_MODE_OCT=y +CONFIG_SPIRAM_SPEED_80M=y +``` + +If a Kconfig name fails on v5.4, fix to the v5.4 equivalent; do not drop PSRAM on S3. + +## VERIFY + +Need `IDF_PATH` or docker `espressif/idf:v5.4`. + +``` +idf.py -B /tmp/af-wroom -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.wroom" -D AUTOFILM_BOARD=wroom set-target esp32 build +idf.py -B /tmp/af-s3 -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.s3" -D AUTOFILM_BOARD=jc4827w543 set-target esp32s3 build +``` + +Pass = both exit 0. If no IDF in environment: document `BLOCKED: no IDF` and still land the files; do not fake binaries. + +## COMMITS + +1. `Add ESP-IDF skeleton for wroom and jc4827w543 boards` +2. (if needed) `Ignore IDF build products` + +## DoD + +- [ ] Both CMake board comps export the same `board.h` API +- [ ] Motor EN disabled at boot on wroom +- [ ] TWDT not disabled +- [ ] Legacy Arduino tree untouched +- [ ] STATUS→DONE diff --git a/docs/megaplans/refactor/P02-ui-cmd-process.md b/docs/megaplans/refactor/P02-ui-cmd-process.md new file mode 100644 index 0000000..2441874 --- /dev/null +++ b/docs/megaplans/refactor/P02-ui-cmd-process.md @@ -0,0 +1,197 @@ +# P02 — ui_cmd + app_process + golden tests + +STATUS: TODO +DEPENDS: P01 +READ: this file, `src/devSequence.cpp` (values only), `docs/megaplans/REFACTOR-MEGAPLAN.md` +OUT: FreeRTOS tasks, HAL, display, Arduino, CI yaml, machine states +FORBIDDEN: mutate recipe numbers; `String`; Arduino headers in new public `.h` + +## IN + +C components + host tests that lock recipe bytes. No firmware behaviour change yet (`main.c` may log process count). + +## FILES + +``` +components/ui_cmd/CMakeLists.txt +components/ui_cmd/include/ui_cmd.h +components/app_process/CMakeLists.txt +components/app_process/include/app_process.h +components/app_process/app_process.c +tests/host/CMakeLists.txt +tests/host/test_process.c +tests/host/main.c # if Unity/CTest needs a runner; else add_executable per test +``` + +Host CMake **must not** require `IDF_PATH`. Link `app_process` sources directly. Provide `esp_err_t` typedef if needed: + +```c +typedef int esp_err_t; +#define ESP_OK 0 +#define ESP_ERR_NOT_FOUND 0x105 +#define ESP_ERR_INVALID_ARG 0x102 +``` + +in `tests/host/esp_host_compat.h` and include it only in host builds. + +## ui_cmd.h (lock) + +Packed, no pointers in the union payload except none — use ids. + +```c +#pragma once +#include +#include + +#define UI_CMD_QUEUE_LEN 8 +#define UI_EVT_QUEUE_LEN 16 + +typedef enum { + CMD_SELECT_PROCESS = 1, + CMD_BROWSE_STEP = 2, + CMD_ADJUST_STEP_TIME = 3, + CMD_ARM_STEP = 4, + CMD_START_STEP = 5, + CMD_STOP = 6, + CMD_RESUME = 7, + CMD_RETURN_TO_STEP_SELECT = 8, + CMD_CANCEL_ARMED = 9, + CMD_PUMP_SET = 100, + CMD_VALVE_SET = 101, + CMD_RESERVOIR_SELECT = 102, + CMD_DRAIN_START = 103, + CMD_DRAIN_STOP = 104, + CMD_HEATER_SETPOINT = 105 +} ui_cmd_id_t; + +typedef enum { + EVT_PROCESS_SELECTED = 1, + EVT_STEP_VIEW = 2, + EVT_STEP_ARMED = 3, + EVT_STEP_STARTED = 4, + EVT_STEP_PROGRESS = 5, + EVT_STEP_COMPLETE = 6, + EVT_STEP_STOPPED = 7, + EVT_STEP_RESUMED = 8, + EVT_PROCESS_IDLE = 9, + EVT_TEMP_UPDATED = 10, + EVT_FAULT = 11 +} ui_evt_id_t; + +typedef enum { MOTION_IDLE = 0, MOTION_CW = 1, MOTION_CCW = 2 } motion_t; + +typedef struct { + ui_cmd_id_t id; + uint8_t process_id; + int8_t step_delta; /* BrowseStep */ + int8_t time_delta_s; /* AdjustStepTime, typically ±5 */ + uint8_t step_index; /* Arm/Start */ +} ui_cmd_t; + +typedef struct { + ui_evt_id_t id; + uint8_t process_id; + uint8_t step_index; + uint8_t step_count; + uint16_t time_s; + uint32_t remaining_ms; + float temp_c; /* NAN if disconnected */ + bool temp_ok; + float temp_min_c, temp_pref_c, temp_max_c; + float cw_revs, ccw_revs; + motion_t motion; + int16_t fault_code; +} ui_evt_t; +``` + +P03+ fill unused fields with 0 / NAN. Do not add `char name[n]` to the event if it forces copies; UI looks up names via `app_process` by id+index. + +## app_process.h (lock) + +```c +#pragma once +#include +#include "esp_err.h" /* or host compat */ + +#define PROCESS_COUNT 6 +#define PROCESS_MAX_STEPS 20 + +enum { + PROC_C41 = 0, + PROC_E6 = 1, + PROC_ECN2 = 2, + PROC_BW = 3, + PROC_CUSTOM = 4, + PROC_BWREV = 5 +}; + +typedef struct { + const char *name; + uint16_t time_s; + float cw_revs, ccw_revs; + float temp_min_c, temp_pref_c, temp_max_c; +} process_step_t; + +typedef struct { + const char *name; + uint8_t step_count; + const process_step_t *steps; +} process_def_t; + +void app_process_init(void); /* zero session deltas */ +const process_def_t *app_process_get(uint8_t id); +const process_def_t *app_process_find_name(const char *name); +uint16_t app_process_time_s(uint8_t id, uint8_t step); /* default + session delta, min 0 */ +esp_err_t app_process_adjust_time(uint8_t id, uint8_t step, int8_t delta_s); +void app_process_reset_session(uint8_t id); +``` + +Session overlay: `static int16_t time_delta_s[PROCESS_COUNT][PROCESS_MAX_STEPS]`. `adjust_time` adds delta; does **not** write `process_step_t.time_s`. Clamp resulting time to `[0, 36000]`. + +## Golden table (assert exact) + +Copy from `src/devSequence.cpp`. Names `strcmp`. Floats: compare with `fabsf(a-b)<1e-4` except integers as exact. + +**C41** `name="C41"` count=7 + +| i | name | t | cw | ccw | min | pref | max | +|---|---|---:|---:|---:|---:|---:|---:| +| 0 | Prewarm | 180 | 1 | 1 | 37.8 | 38 | 38.2 | +| 1 | Developer | 195 | 5.5 | 5 | 37.8 | 38 | 38.2 | +| 2 | Bleach | 45 | 5.5 | 5 | 32 | 38 | 38.2 | +| 3 | Fix | 180 | 5.5 | 5 | 32 | 38 | 38.2 | +| 4 | Rinse 1 | 60 | 3.5 | 3 | 32 | 38 | 38.2 | +| 5 | Rinse 2 | 60 | 3.5 | 3 | 32 | 38 | 38.2 | +| 6 | Fin Rinse | 30 | 1 | 1 | 32 | 38 | 38.2 | + +**E6** `name="E6"` 12: times `{180,360,120,120,360,120,360,240,120,120,120,30}` names `Preheat,FirstDev,Wash 1,Reversal,ColorDev,PreBleach,Bleach,Fixer,Wash 2,Wash 3,Wash 4,Fin Rinse` cw `{1,5.5,3.5,5,5.5,5.5,5.5,5.5,3.5,3.5,3.5,3.5}` ccw `{1,5,3,5.5,5,5.5,5.5,5.5,3,3,3,3}` min `{37.5,37.7,33.0,37.7,37.0,37,37.5,37.5,33.0,33.0,33.0,19.0}` pref `{38,38,38,38,38,38,38,38,38,38,38,20}` max `{38.5,38.3,38.0,38.3,39.0,38,38.5,38.5,38.5,38.5,38.5,21.0}` + +**ECN-2** `name="ECN-2"` 9: t `{180,0,210,60,180,150,120,300,120}` names `Prebath,RemJet,Developer,Stop Bath,Wash,Bleach,Fixer,Wash 2,Fin Rinse` cw `{1,0,5.5,3.5,3.5,5.5,5.5,3.5,1}` ccw `{1,0,5,3,3,5,5,3,1}` min `{27,0,40.8,27,27,27,27,27,27}` pref `{38,0,41,38,38,38,38,38,38}` max `{38,0,41.2,38,38,38,38,38,38}` + +**B&W** `name="B&W"` 7: t `{510,30,300,60,90,120,30}` names `Developer,Stop,Fix,Rinse 1,Rinse 2,Rinse 3,Fin Rinse` cw `{5.5,3.5,3.5,3.5,3.5,3.5,1}` ccw `{5,3,3,3,3,3,1}` min 19 pref 20 max 21 all. + +**Custom** `name="Custom"` 4: t 10, names `Developer,Stop,Fix,Rinse` cw `{5.5,3.5,3.5,3.5}` ccw `{5,3,3,3}` min 19 pref 20 max 21. + +**B&WREV** `name="B&WREV"` 12: t `{720,300,300,60,120,60,120,360,60,300,60,60}` names `FirstDev,Wash 1,Bleach,Wash 2,Clearing,Wash 3,Reversal,SecondDev,Wash 4,Fix,Wash 5,Fin Rinse` cw `{5.5,3.5,5.5,3.5,5.5,3.5,5.5,5.5,3.5,5.5,3.5,3.5}` ccw `{5,3,5,3,5,3,5,5,3,5,3,3}` min `{19.5,15.5,19.5,15.5,19.5,15.5,19.5,19.5,15.5,19.5,15.5,15.5}` pref 20 all max `{20.5,22.5,22.5,22.5,22.5,22.5,22.5,22.5,22.5,22.5,22.5,22.5}` + +Tests also: `find_name("nope")==NULL`; `adjust_time(C41,1,+5)` → 200, const table still 195; `reset_session` restores 195. + +## VERIFY + +``` +cmake -S tests/host -B build/host && cmake --build build/host && ctest --test-dir build/host --output-on-failure +``` + +All golden tests pass. + +## COMMITS + +1. `Add ui_cmd and app_process with frozen recipe tables` +2. `Add host tests for process lookup and session time overlay` + +## DoD + +- [ ] Headers Arduino-free +- [ ] Every cell in golden table asserted +- [ ] Overlay does not mutate const steps +- [ ] STATUS→DONE diff --git a/docs/megaplans/refactor/P03-machine-host.md b/docs/megaplans/refactor/P03-machine-host.md new file mode 100644 index 0000000..b80a336 --- /dev/null +++ b/docs/megaplans/refactor/P03-machine-host.md @@ -0,0 +1,136 @@ +# P03 — app_machine + stub HAL + state tests + +STATUS: TODO +DEPENDS: P02 +READ: this file, `docs/megaplans/REFACTOR-MEGAPLAN.md`, `components/ui_cmd/include/ui_cmd.h` +OUT: real GPIO, Arduino, LCD, Gitea yaml, deleting `src/menu.cpp` +FORBIDDEN: `vTaskDelete`; blocking `delay` in machine logic; auto_advance default true + +## IN + +Synchronous state machine callable from host tests **without FreeRTOS**. On-target, P06 wraps it in `machine_task`. Same `.c` file compiled twice. + +## FILES + +``` +components/app_machine/CMakeLists.txt +components/app_machine/include/app_machine.h +components/app_machine/app_machine.c +components/hal_motor/include/hal_motor.h +components/hal_temp/include/hal_temp.h +components/hal_audio/include/hal_audio.h +tests/host/stubs/hal_motor.c +tests/host/stubs/hal_temp.c +tests/host/stubs/hal_audio.c +tests/host/test_machine.c +``` + +Host CMake adds stubs + `app_machine.c` + `app_process.c`. Define `AUTOFILM_HOST=1` for clock injection. + +## HAL headers (lock; P05 implements for device) + +`hal_motor.h`: + +```c +void hal_motor_init(void); +void hal_motor_enable(bool on); /* on=false → EN disabled (HIGH on wroom) */ +void hal_motor_request_stop(void); /* must be safe to call anytime; sets flag + enable false */ +esp_err_t hal_motor_agitate_start(float cw_revs, float ccw_revs, uint32_t rpm); +void hal_motor_agitate_stop(void); /* leave disabled */ +bool hal_motor_is_enabled(void); +``` + +`hal_temp.h`: `void hal_temp_init(void); esp_err_t hal_temp_read_c(float *out);` /* ESP_FAIL → disconnected */ + +`hal_audio.h`: `void hal_audio_init(void); void hal_audio_beep_short(void); void hal_audio_alarm_complete(void); void hal_audio_alarm_cancel(void);` + +Host stubs: record last call in globals `stub_motor_enabled`, `stub_stop_count`, `stub_beep_count`, `stub_alarm_count`, `stub_alarm_cancel_count`. `hal_temp_read_c` returns 20.0 ESP_OK unless test sets `stub_temp_fail`. + +## app_machine.h (lock) + +```c +typedef enum { + ST_IDLE = 0, + ST_STEP_SELECT, + ST_ARMED, + ST_RUNNING, + ST_STOPPED, + ST_COMPLETE +} machine_state_t; + +void app_machine_init(void); +machine_state_t app_machine_state(void); +void app_machine_handle_cmd(const ui_cmd_t *cmd); +void app_machine_tick(uint32_t now_ms); /* drive timers; tests call this */ +void app_machine_on_temp(float c, bool ok); +int app_machine_last_event(ui_evt_t *out); /* pop 1 event; 0 empty, 1 ok */ +uint8_t app_machine_process_id(void); +uint8_t app_machine_step_index(void); +uint32_t app_machine_remaining_ms(void); +``` + +Internal event ring size `UI_EVT_QUEUE_LEN`. + +## Transitions (implement exactly) + +| from | cmd/tick | to | side effects | +|---|---|---|---| +| IDLE | SELECT_PROCESS valid id | STEP_SELECT | evt PROCESS_SELECTED + STEP_VIEW(0) | +| STEP_SELECT | BROWSE_STEP +1/-1 clamp | STEP_SELECT | STEP_VIEW | +| STEP_SELECT | ADJUST_STEP_TIME ±5 | STEP_SELECT | overlay; STEP_VIEW | +| STEP_SELECT | ARM_STEP or START_STEP | ARMED | STEP_ARMED (START from select still arms first — do not start motion until START in ARMED, except if cmd is START_STEP from ARMED) | +| ARMED | START_STEP | RUNNING | motor enable+agitate_start; remaining=time_s*1000; deadline=now+remaining; STEP_STARTED | +| ARMED | CANCEL_ARMED or STOP | STEP_SELECT | motor disabled; STEP_VIEW | +| RUNNING | tick now>=deadline | COMPLETE | agitate_stop; enable false; alarm_complete; STEP_COMPLETE | +| RUNNING | STOP | STOPPED | **request_stop + enable false first**; beep_short; remaining=deadline-now; STEP_STOPPED | +| STOPPED | RESUME | RUNNING | beep_short; deadline=now+remaining; agitate_start; STEP_RESUMED | +| STOPPED | RETURN_TO_STEP_SELECT or STOP | STEP_SELECT | enable false; remaining stored in overlay as ceil(remaining/1000) replacing session time for that step; STEP_VIEW | +| COMPLETE | START_STEP / ARM_STEP next | ARMED of next if any else IDLE PROCESS_IDLE | | +| COMPLETE | RETURN_TO_STEP_SELECT | STEP_SELECT | | +| COMPLETE | STOP | STEP_SELECT | alarm_cancel; beep_short | +| * | STOP in RUNNING | (above) | STOP must work even if alarm would be playing — in COMPLETE STOP cancels alarm | +| * | invalid id | stay | EVT_FAULT code=1 | + +START_STEP from STEP_SELECT: treat as ARM then if you want one-key start, **still require second START** (matches Ent on armed screen). Keypad `E` on step list currently starts processing after a prompt — two-step is correct. + +`auto_advance`: `static bool auto_advance=false`. If true, COMPLETE tick auto ARM next. Tests assert default false: after COMPLETE, state stays COMPLETE until cmd. + +RPM for agitate_start: 60. + +0-second steps (ECN-2 RemJet): START → immediately COMPLETE on next tick (no motor enable, or enable false). Tests cover this. + +## Clock + +`app_machine_tick(now_ms)` monotonic. Tests: init, SELECT C41, ARM 0, START, tick(0) start, tick(179999) still RUNNING, tick(180000) COMPLETE. Custom 10s similar. + +STOP at t=1000 on Custom step0: remaining_ms ≈ 9000±tick, `stub_motor_enabled==false`, `stub_stop_count>=1`. RESUME then tick +9000 → COMPLETE. + +## VERIFY + +``` +cmake -S tests/host -B build/host && cmake --build build/host && ctest --test-dir build/host --output-on-failure +``` + +Required cases in `test_machine.c` names: + +- `select_c41_step_view` +- `adjust_does_not_mutate_const` +- `arm_start_complete_custom_10s` +- `stop_disables_motor_and_resume` +- `return_from_stopped` +- `stop_during_complete_cancels_alarm` +- `ecn2_remjet_zero_time` +- `stop_ignored_meaningless_in_idle` (IDLE+STOP → stay IDLE, no motor calls required) + +## COMMITS + +1. `Add HAL headers and host stubs for motor temp audio` +2. `Add app_machine state machine with stop and resume` +3. `Add host tests for machine stop resume and complete` + +## DoD + +- [ ] STOP path calls `hal_motor_request_stop` before any other work +- [ ] default auto_advance false +- [ ] host tests cover list above +- [ ] STATUS→DONE diff --git a/docs/megaplans/refactor/P04-gitea-ci.md b/docs/megaplans/refactor/P04-gitea-ci.md new file mode 100644 index 0000000..497f9a3 --- /dev/null +++ b/docs/megaplans/refactor/P04-gitea-ci.md @@ -0,0 +1,55 @@ +# P04 — Gitea Actions CI + +STATUS: TODO +DEPENDS: P01 (required), P02/P03 (tests job becomes real when `tests/host/CMakeLists.txt` exists) +READ: this file, `docs/CICD.md` +OUT: changing firmware sources; enabling branch protection yourself if you lack repo admin — then note BLOCKED for humans +FORBIDDEN: GitHub-only `espressif/esp-idf-ci-action` as the compile driver; secrets in yaml; required-check flip if firmware still does not compile + +## IN + +Add `.gitea/workflows/ci.yml` per `docs/CICD.md` example. Use `hashFiles` guards: + +- test job runs cmake/ctest only if `tests/host/CMakeLists.txt` exists +- firmware job runs idf.py only if root `CMakeLists.txt` exists (P01) + +Container: `docker.io/espressif/idf:v5.4`. `fail-fast: false`. `needs: test` on firmware. + +Artifact names: `firmware-${{ matrix.board }}` including merged bin, bootloader, partition-table, flasher_args, sdkconfig, size txt. + +Gitea context: try `${{ gitea.sha }}`; if runner only has `github.sha`, use that. Document which in Notes. + +Triggers exactly: + +```yaml +on: + pull_request: + branches: [develop] + push: + branches: [develop, "refactor/**", "feature/**"] + tags: ["v*"] + workflow_dispatch: +``` + +`runs-on: ubuntu-latest`. + +Firmware matrix: wroom/esp32/sdkconfig.wroom and jc4827w543/esp32s3/sdkconfig.s3. + +`git config --global --add safe.directory '*'` inside container before idf.py. + +Do **not** add a release-attach job unless tag attach API is already used on this Gitea; skip release job in v1. + +## VERIFY + +- yaml parses (python `-c 'import yaml; yaml.safe_load(open(".gitea/workflows/ci.yml"))'` if PyYAML present, else visual: valid YAML, two-space indent) +- If Gitea runner visible: push and confirm jobs queued. If not: still land file; Notes=`runner not verified` + +## COMMITS + +1. `Add Gitea Actions workflow for host tests and dual firmware` + +## DoD + +- [ ] Path `.gitea/workflows/ci.yml` +- [ ] Guards prevent red on missing tests/host before P02 +- [ ] STATUS→DONE diff --git a/docs/megaplans/refactor/P05-hal-wroom-motion.md b/docs/megaplans/refactor/P05-hal-wroom-motion.md new file mode 100644 index 0000000..f32c901 --- /dev/null +++ b/docs/megaplans/refactor/P05-hal-wroom-motion.md @@ -0,0 +1,95 @@ +# P05 — WROOM HAL motor / temp / audio + +STATUS: TODO +DEPENDS: P03 +READ: this file, `include/config.h`, `src/motor.cpp`, `src/temperature.cpp`, `src/sound.cpp`, `components/hal_*/include/*.h` +OUT: rewriting `app_ui` / deleting menus; S3 motor pins; disabling WDT; `vTaskDelete` motor +FORBIDDEN: new Arduino libraries; `tone()` on machine task; blocking OneWire on UI task + +## IN + +Device implementations of `hal_motor`, `hal_temp`, `hal_audio` for `board_wroom`. Arduino-as-component **first allowed here**. + +Keep AccelStepper behind `hal_motor`. Inner step loop **must** sample `stop_req` every `stepper.run()` and exit. Motor task is immortal: wait on notification for next `agitate_start`. + +## Arduino component + +`main/idf_component.yml`: + +```yaml +dependencies: + espressif/arduino-esp32: "~3.1.0" +``` + +If 3.1 conflicts with IDF 5.4, pin the combo that compiles; Notes the versions. Only WROOM firmware enables Arduino this phase; S3 may compile without motor SRCS via `if(AUTOFILM_BOARD STREQUAL wroom)`. + +AccelStepper: add as managed component **or** vendor `components/third_party/AccelStepper` (upstream 1.64). Prefer `idf_component.yml` git dep if it builds; else vendor the .cpp/.h used today. + +OneWire + DallasTemperature: same policy. Keypad/LCD **not** this phase. + +## Pins (wroom only, from config) + +| fn | gpio | +|---|---| +| STEP | 12 | +| DIR | 14 | +| EN active-low | 27 | +| DS18B20 | 13 | +| beep | 25 | + +`STEPS_PER_REV=4800` `RPM=60` `ACCEL=9600` `TEMP_OFFSET=0.4f` + +## motor_task + +Priority high, core 0. Loop: + +``` +wait notify +while !stop_req && agitating: + move CW cw_revs (run loop checks stop_req) + if stop_req break + move CCW ccw_revs +hal_motor_enable(false) +``` + +`hal_motor_request_stop`: atomic stop_req=1; `gpio_set_level(EN,1)` **immediately**; notify. ISR-safe: no heap, no stepper API. + +Never `vTaskDelete`. + +## temp_task + +Low prio. Period: request conversion, `vTaskDelay(750ms)`, read, add 0.4, `app_machine_on_temp`. On disconnect `ok=false`. Do not write LCD. + +## audio_task + +Queue of `{SHORT, ALARM, CANCEL}`. SHORT: one 2 kHz 80–120 ms pulse (LEDC or Arduino `tone` **in this task only**). ALARM: 10× (2000 Hz 500 ms on / 250 ms off) **abort immediately on CANCEL or STOP queued**. Machine never `delay`s for sound. + +## main.c (wroom) + +`hal_*_init`, `app_process_init`, `app_machine_init`, create motor/temp/audio tasks. Do not start UI yet (P06). Optional: 1 Hz log remaining if RUNNING for bring-up. + +S3 build: stub motor/temp/audio weak symbols or board-local no-ops so P01 S3 still links. + +## VERIFY + +Host tests still pass. + +WROOM `idf.py` build pass. + +Manual on hardware (if unavailable, Notes=`hw not flashed`; still land code): + +1. Custom start (will need P06 for keypad — this phase may expose a 5 s auto-demo **behind `#ifdef AUTOFILM_MOTION_SMOKE` default off**). Do not enable smoke in production sdkconfig. +2. Logic-analyzer optional. Minimum: compile + review EN init HIGH. + +## COMMITS + +1. `Add Arduino-as-component and WROOM motor HAL with cooperative stop` +2. `Add temp and audio tasks off the UI path` + +## DoD + +- [ ] EN HIGH at init and on stop +- [ ] stop_req checked inside step loop +- [ ] OneWire only in temp_task +- [ ] tone/LEDC only in audio_task +- [ ] STATUS→DONE diff --git a/docs/megaplans/refactor/P06-ui-wroom-cutover.md b/docs/megaplans/refactor/P06-ui-wroom-cutover.md new file mode 100644 index 0000000..7000d74 --- /dev/null +++ b/docs/megaplans/refactor/P06-ui-wroom-cutover.md @@ -0,0 +1,137 @@ +# P06 — WROOM UI cutover (2004 + keypad + Stop/Resume) + +STATUS: TODO +DEPENDS: P05 +READ: this file, `src/menu.cpp`, `src/display.cpp`, `src/config.cpp`, `docs/TARGET_ARCHITECTURE.md` (screens + stop rules) +OUT: S3 LVGL, NVS profiles, keeping blocking `getEntEscInput` as the live path +FORBIDDEN: `while(true)` keypad wait in machine; `malloc` for mm:ss; `vTaskDelete`; WDT off + +## IN + +Replace live control path with `input_task` + `ui_task` + `app_ui` screens on 20×4. Legacy `src/*.cpp` may remain unlinked. `main` no longer calls `startingMenu()`. + +After this phase the WROOM firmware **is** the product. + +## FILES + +``` +components/hal_display/include/hal_display.h +components/hal_display/hal_display_lcd2004.cpp +components/hal_input/include/hal_input.h +components/hal_input/hal_input_keypad.cpp +components/app_ui/include/app_ui.h +components/app_ui/app_ui.c +components/app_ui/screens.h +``` + +C++ only in hal_display / hal_input. + +## hal_display.h + +```c +void hal_display_init(void); +void hal_display_clear(void); +void hal_display_text(int col, int row, const char *s); /* clip to 20 cols */ +void hal_display_glyph_thermo(int col, int row); +void hal_display_flush(void); /* no-op */ +``` + +LCD: I2C 21/22 addr 0x27, 20x4, createChar thermometer from `config.cpp` bytes. + +mm:ss: stack buffer `char[6]`, `snprintf "%02d:%02d"`. Never malloc. + +## hal_input.h + +```c +typedef struct { char key; } ui_raw_key_t; +void hal_input_init(void); +bool hal_input_pop_key(ui_raw_key_t *out); /* nonblocking */ +``` + +Keypad map **byte-identical** to `src/config.cpp`. Scan in `input_task` 25 Hz. Debounce via Keypad lib. + +Map: + +| key | cmd | +|---|---| +| 1..6 | SELECT_PROCESS id 0..5 | +| U/D | BROWSE_STEP ±1 | +| L/R | ADJUST_STEP_TIME ±5 | +| E | ARM if STEP_SELECT; START if ARMED; RESUME if STOPPED; ARM next if COMPLETE | +| X | CANCEL_ARMED if ARMED; STOP if RUNNING or COMPLETE; RETURN_TO_STEP_SELECT if STOPPED; ignore IDLE | + +**X in RUNNING:** `hal_motor_request_stop()` **in input_task immediately**, then queue CMD_STOP. Dual-path required. + +## Screens (20×4) + +`ProgramSelect` (IDLE): + +``` +Select Programme: +1. C41 4. ECN-2 +2. E6 5. Custom +3. B&W 6. B&W Rev +``` + +`StepSelect`: + +``` +Step Time Temp + mm:ss nnC +Scroll / Esc / Ent + 🌡 tt.tC row3 col13 thermo+temp or -- +``` + +`Armed`: name, mm:ss, pref C, `Ent:start Esc:quit`, temp row3 + +`Running`: name row0, `Remaining: mm:ss` row1, temp row3. Update remaining ≥4 Hz from events, **not** by blocking. No `delay(410)`. + +`Stopped`: name, remaining, `E:resume X:back`, temp row3 + +`Complete`: name `Done`, then alarm (nonblocking). `E` next / `X` back. + +## Tasks + +| task | prio | +|---|---| +| input_task | high | +| machine_task | mid-high; `handle_cmd` + `tick(esp_timer_get_time/1000)` 50 Hz | +| ui_task | mid; drain events, dirty draw | +| motor/temp/audio | as P05 | + +TWDT: register tasks that run long loops or rely on idle. + +## Cutover + +`main.c`: init display/input, splash `AUTOFILM` + git describe 1 s **via vTaskDelay not busy Arduino delay in ui_task after scheduler start**. Unlink `src/AutoFilmESP32.cpp` from IDF (leave files on disk). + +`platformio.ini` may stay; do not promise PIO still builds. + +## VERIFY + +Host tests pass. + +WROOM idf build pass. + +Hardware smoke: + +1. Select Custom, Ent to arm, Ent to start 10 s step, remaining counts, temp updates +2. Mid-step X: motor stops at once, Stopped screen, E resumes remaining, X returns to list +3. Disconnect DS18B20: `--` still navigable +4. Complete alarm: X cancels and returns; UI not frozen 7.5 s + +If no hardware: still land; Notes=`hw smoke pending`; do not skip the input dual-path stop. + +## COMMITS + +1. `Add 2004 display and keypad HAL` +2. `Add app_ui screens driven by machine events` +3. `Cut over WROOM main to RTOS UI and stop-safe input` + +## DoD + +- [ ] Esc during run always EN-off +- [ ] No malloc mm:ss +- [ ] No blocking alarm on UI/machine +- [ ] Legacy menus not in the IDF link +- [ ] STATUS→DONE diff --git a/docs/megaplans/refactor/P07-board-s3-ui.md b/docs/megaplans/refactor/P07-board-s3-ui.md new file mode 100644 index 0000000..958af1a --- /dev/null +++ b/docs/megaplans/refactor/P07-board-s3-ui.md @@ -0,0 +1,58 @@ +# P07 — JC4827W543 display + GT911 + STOP + +STATUS: TODO +DEPENDS: P06 +READ: this file, `docs/TARGET_ARCHITECTURE.md` dual-board + UI strategy, vendor pin notes below +OUT: LVGL in `app_machine`; changing recipes; assigning WROOM STEP/DIR onto S3 without a pin table comment; pumps +FORBIDDEN: `lv_*` types in `ui_cmd.h` / `app_machine.h`; blocking in flush_cb beyond DMA wait; copying WROOM GPIO map blindly + +## IN + +S3 firmware shows the **same five screens** via a colour adapter. Touch STOP = same path as Esc (`hal_motor_request_stop` + CMD_STOP). Custom draw first (no LVGL unless bring-up is otherwise blocked — if LVGL used, confine to `hal_display` + `app_ui` board files). + +Mechatronics on S3: keep motor EN `GPIO_NUM_NC` **unless** a spare pin is documented in `board.c` comments with the header pin number. Do not invent STEP/DIR. S3 image is operator-panel-capable; agitation may remain on WROOM until pins proven. + +## Pins (lock from community JC4827W543C) + +Display NV3041A QSPI: CS 45, SCK 47, D0 21, D1 48, D2 40, D3 39, BL 1, 480×272. + +GT911 I2C: SDA 8, SCL 4, INT 3, RST 38, addr 0x5D. Reset: RST low 200 ms then high; INT as input. + +PSRAM octal 80 MHz already in `sdkconfig.s3`. + +Free IO is scarce. Do not steal QSPI/GT911 pins for motor. + +## FILES + +``` +components/board_jc4827w543/board.c # update comments/pins +components/hal_display/hal_display_nv3041a.c # or .cpp +components/hal_input/hal_input_gt911.c +components/app_ui/app_ui_color.c # or ifdefs in app_ui.c +``` + +`hal_display_text` still works (glyph blit at cell grid 24×8 or similar) so `app_ui.c` can stay shared. Prefer: `app_ui` calls abstract `ui_draw_program_select()` implemented per board. + +STOP hit-test: rectangle bottom of Running/Stopped/Complete screens, label `STOP`. On touch inside: `hal_motor_request_stop()` then CMD_STOP (same as keypad X). Resume/Back buttons on Stopped. + +## VERIFY + +S3 `idf.py` build pass. + +WROOM build still pass (do not regress P06). + +Host tests pass. + +Hardware S3: ProgramSelect visible, tap process, Running shows name/remaining/temp, STOP stops if motor wired else still transitions UI to Stopped. + +## COMMITS + +1. `Add NV3041A and GT911 HAL for jc4827w543` +2. `Map colour screens and STOP hit-test to existing commands` + +## DoD + +- [ ] Same commands as keypad +- [ ] No lv types in machine/process/cmd headers +- [ ] WROOM firmware unchanged in behaviour +- [ ] STATUS→DONE diff --git a/docs/megaplans/refactor/P08-idf-debt.md b/docs/megaplans/refactor/P08-idf-debt.md new file mode 100644 index 0000000..ba40a0a --- /dev/null +++ b/docs/megaplans/refactor/P08-idf-debt.md @@ -0,0 +1,40 @@ +# P08 — Arduino debt burn-down + +STATUS: TODO +DEPENDS: P06 (behaviour parity). P07 optional. +READ: this file, `components/hal_motor/**`, HAL headers from P03 +OUT: recipe edits; UI redesign; LVGL-by-default +FORBIDDEN: behaviour change of times/stop/resume; dropping cooperative stop + +## IN + +Replace Arduino libs with IDF peripherals **behind the same HAL**. Remove Arduino-as-component from the S3 image first, then WROOM. + +Order (one commit each, tests+builds green after each): + +1. **Audio:** LEDC / `ledc_timer` tones; delete `tone()`. +2. **Motor:** RMT TX pulse train for STEP, GPIO DIR/EN. `hal_motor_request_stop` aborts RMT + EN HIGH. Delete AccelStepper. Same 4800 steps/rev, 60 RPM, accel ≈9600 (RMT may use a short ramp table). +3. **Temp:** RMT or `onewire_bus` IDF component; keep +0.4 °C offset. Delete Dallas/OneWire Arduino. +4. **Keypad:** GPIO matrix scan in `hal_input` (no Keypad lib). Same keymap. +5. **LCD 2004:** IDF I2C + HD44780 nibble commands (or `esp_lcd` panel io i2c). Delete LiquidCrystal_I2C. +6. Drop `espressif/arduino-esp32` from `idf_component.yml` when nothing includes Arduino.h. + +Do not change `hal_*` public headers except adding RMT-specific internals in `.c`. + +## VERIFY + +Host tests pass (stubs unchanged). + +Both boards build without Arduino component. + +Hardware WROOM: Custom 10 s, Esc mid-step EN off, Resume remaining, temp display, beep Stop/Resume, complete alarm cancellable. + +## COMMITS + +Per numbered item above, e.g. `Replace AccelStepper with RMT step pulse HAL`. + +## DoD + +- [ ] `rg -n "Arduino.h|AccelStepper|LiquidCrystal|DallasTemperature|#include |#include "` empty in `main/` `components/` +- [ ] Stop still EN-off first +- [ ] STATUS→DONE