diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8853fe5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,87 @@ +# Agent conventions — AutoFilm-ESP32 + +Instructions for humans and coding agents working in this repository. + +## Product constraints + +- Default temperatures, step times, and CW/CCW rotation counts for C41, E6, ECN-2, B&W, Custom, and B&WREV **do not change** unless a commit message explicitly says so and the recipe table in `docs/CURRENT_STATE.md` is updated in the same change. +- The machine **must always be stoppable** while a step is running: keypad Esc (`'X'`, 4 across / 4 down) and, on the S3 panel, a STOP control. Stop drops motor enable immediately, then offers Resume or return to step select. +- UI must not block on motor motion, OneWire conversion, or melody playback. + +## Branch and docs + +- Active refactor branch: `refactor/esp-idf-modular-ui`. +- `main` is the pre-refactor PlatformIO Arduino tree. +- Architecture intent: `docs/CURRENT_STATE.md`, `docs/TARGET_ARCHITECTURE.md`. Update those docs when the design changes; do not leave the tree contradicting them. +- Do not commit secrets (Git tokens, Wi-Fi passwords, `sdkconfig` with provisioned keys). + +## Layout + +| Path | Role | +| --- | --- | +| `src/`, `include/` | Legacy Arduino sources; shrink, do not grow, except bugfixes needed for the port | +| `main/` | IDF entry | +| `components/app_*` | Application: process table, machine, UI | +| `components/ui_cmd` | Command/event types only | +| `components/hal_*` | Hardware adapters | +| `components/board_*` | Pin maps and board bring-up | +| `docs/` | Design notes | + +New behaviour goes into `components/`, not into larger `menu.cpp` files. + +## Framework policy + +- Build system is **ESP-IDF**. PlatformIO + Arduino remains only until the IDF skeleton compiles both boards. +- **Arduino-as-component** is allowed as a bridge for AccelStepper, Keypad, LiquidCrystal_I2C, OneWire, DallasTemperature. +- Treat Arduino as debt: + - No Arduino types (`String`, `Keypad`, `AccelStepper`, `LiquidCrystal_I2C`) in headers of `app_machine`, `app_process`, or `ui_cmd`. + - Do not add new Arduino library dependencies. + - Prefer IDF drivers (GPIO, RMT, I2C, LEDC, `esp_timer`, `esp_lcd`) for new work. +- Two firmware flavours: `board_wroom` and `board_jc4827w543`. Gate pins and peripherals with board CMake, not `#ifdef` sprinkled through the state machine. + +## Concurrency + +- One responsibility per task: input, UI, machine, motor, temperature, audio. +- Cross-task data: FreeRTOS queues, task notifications, atomics. No unsynchronised use of `lcd` / `stepper` / `sensors`. +- ISR / stop path: set a flag, disable motor EN, `xQueueSendFromISR` / `xTaskNotifyFromISR`. No heap, no `delay`, no LCD from ISR. +- Do not `vTaskDelete` worker tasks after startup. Do not disable the task watchdog to “make it work”. +- `vTaskDelay` / `esp_timer` instead of busy-wait `millis()` loops. + +## UI / machine split + +- UI emits commands; machine emits events. See `docs/TARGET_ARCHITECTURE.md`. +- Touch and keypad are input adapters. They produce the same commands. +- Display adapters render events. Machine code must compile with no display driver linked (except in tests that stub HAL). + +## Code style + +- C11 for new IDF components; C++17 only where an existing Arduino library forces it, isolated in `hal_*`. +- `snake_case` for new functions and files; existing camelCase stays until that file is rewritten. +- 4-space indent to match the current tree. +- No raw `new` / `malloc` for per-frame strings. No `String` concatenation on the control path. +- Log with `ESP_LOGx` tags (`machine`, `motor`, `ui`, `temp`, `input`), not `Serial.print`. +- Errors are `esp_err_t` across HAL boundaries. + +## Recipes + +- Recipe tables live in `app_process` (or today’s `devSequence.cpp` until moved). +- Session time nudges (±5 s) are RAM overlays, not edits of the const defaults. +- Persistent custom profiles are out of scope until designed; do not half-add NVS. + +## Hardware notes + +- WROOM pins are defined in the legacy `config` headers; do not silently reuse them on the S3 panel. +- JC4827W543C: NV3041A 480×272 QSPI, GT911 on I²C, limited free IO. Confirm any new GPIO against the board pin table before assigning STEP/DIR/EN/1-Wire. +- Motor enable is active LOW on the current WROOM wiring. Safe state is disabled. + +## Commits and review + +- Imperative commit subjects (`Add machine stop notify`, not `Added`). +- One concern per commit when practical (HAL wrap ≠ UI rewrite ≠ recipe move). +- Do not reformat whole legacy files in the same commit as a behaviour change. +- If defaults would change, fail the change unless the user explicitly asked. + +## Tests + +- Prefer host-side tests for `app_process` lookup and the machine state machine with a stub motor. +- On-target smoke: select process, start a short Custom step, Esc mid-step, confirm EN high and Resume works. diff --git a/docs/CURRENT_STATE.md b/docs/CURRENT_STATE.md new file mode 100644 index 0000000..6b58dcc --- /dev/null +++ b/docs/CURRENT_STATE.md @@ -0,0 +1,167 @@ +# AutoFilm-ESP32 — Current State + +Branch baseline: `main` at `9186d84` (`Add LICENSE`). +This document describes the tree **as it exists today**, before the ESP-IDF modular refactor. Recipe *values* (times, temperatures, rotation counts) are treated as frozen; this document does not propose changing them. + +## What the product is + +AutoFilm-ESP32 is a Kindermann-style rotary film processor controller. The operator picks a process (C41, E6, B&W, ECN-2, Custom, B&WREV), walks the chemical steps, starts a timed agitation cycle, and is prompted to pour / drain between steps. Temperature is *measured* only; closed-loop bath heat is an external sous-vide. + +## Toolchain and layout + +| Item | Today | +| --- | --- | +| Build | PlatformIO `platformio.ini`, env `esp32dev` | +| Framework | Arduino on Espressif32 | +| Target | Classic ESP32-WROOM (`board = esp32dev`) | +| Layout | `src/*.cpp`, `include/*.h`, Arduino-style `setup()` / `loop()` | +| Entry | `src/AutoFilmESP32.cpp` | + +Arduino libraries (all treated as future debt): + +- `AccelStepper` — STEP/DIR stepper +- `LiquidCrystal_I2C` — 20×4 character LCD +- `Keypad` — matrix scan +- `OneWire` + `DallasTemperature` — DS18B20 + +There is **no** ESP-IDF `CMakeLists.txt`, no `sdkconfig`, no component split, no CI matrix. + +## Hardware as wired in this tree + +From `include/config.h` and `src/config.cpp`: + +| Function | GPIO / bus | +| --- | --- | +| Stepper STEP (pulse) | 12 | +| Stepper DIR | 14 | +| Stepper EN (active LOW) | 27 | +| DS18B20 data | 13 | +| Beeper | 25 | +| LCD I²C | SDA 21, SCL 22, address `0x27`, 20×4 | +| Keypad rows | 19, 18, 5, 17, 16 | +| Keypad cols | 15, 2, 0, 4 | + +Motor constants: `STEPS_PER_REV = 4800`, `RPM = 60`, acceleration `9600`. Enable is driven HIGH at boot (disabled). + +Keypad map (5×4): + +``` +F E # * +1 2 3 U +4 5 6 D +7 8 9 X ← 4 across, 4 down = Esc ('X') +L 0 R E +``` + +Keys `1`–`6` select programmes. `U`/`D` browse steps. `L`/`R` nudge step time ±5 s. `E` confirm. `X` escape. + +## Module map + +``` +AutoFilmESP32.cpp setup/loop, LCD splash, watchdog off +config.cpp / .h globals, pin map, library objects +devSequence.cpp/.h six compiled-in recipes +menu.cpp / .h blocking menus + startProcessing() +display.cpp / .h starting menu, headings, mm:ss helper +temperature.cpp/.h DS18B20 read + LCD write +motor.cpp / .h FreeRTOS agitation task +sound.cpp / .h blocking tone() tunes +watchdog.h disables TWDT and idle-task WDT +``` + +There is no HAL. UI, recipes, and actuators share globals (`devPgm`, `run`, `lcd`, `stepper`, `keypad`, task handles). + +## Recipe data + +`struct devSequence` (`include/devSequence.h`): + +- `processName[7]` +- `cycles` — number of steps +- `processTime[20]` — seconds +- `processCycleName[20][10]` +- `processCycle[2][20]` — `[0]` CW revolutions, `[1]` CCW revolutions +- `processTemp[3][20]` — `[0]` min, `[1]` preferred, `[2]` max °C + +`NUM_DEV_SEQUENCES = 6`. Lookup is linear `strcmp` in `findSequenceByName()`. + +### Compiled defaults (do not change values) + +**C41** — 7 steps, ~38 °C developer: + +| Step | Time s | Name | CW | CCW | Min | Pref | Max | +| --- | ---: | --- | ---: | ---: | ---: | ---: | ---: | +| 0 | 180 | Prewarm | 1 | 1 | 37.8 | 38 | 38.2 | +| 1 | 195 | Developer | 5.5 | 5 | 37.8 | 38 | 38.2 | +| 2 | 45 | Bleach | 5.5 | 5 | 32 | 38 | 38.2 | +| 3 | 180 | Fix | 5.5 | 5 | 32 | 38 | 38.2 | +| 4 | 60 | Rinse 1 | 3.5 | 3 | 32 | 38 | 38.2 | +| 5 | 60 | Rinse 2 | 3.5 | 3 | 32 | 38 | 38.2 | +| 6 | 30 | Fin Rinse | 1 | 1 | 32 | 38 | 38.2 | + +**E6** — 12 steps (Preheat … Fin Rinse). FirstDev 360 s at 38 °C; ColorDev 360 s; Fin Rinse 30 s at 19–21 °C. + +**ECN-2** — 9 steps. RemJet is a 0 s / 0 rotation placeholder (manual). Developer 210 s at 40.8–41.2 °C. + +**B&W** — 7 steps. Developer 510 s at 19–21 °C. + +**Custom** — 4×10 s stub (Developer / Stop / Fix / Rinse). + +**B&WREV** — 12 steps. FirstDev 720 s; SecondDev 360 s; wash steps cooler (min 15.5 °C). + +Left/right time edits mutate the in-RAM struct. They are **not** written to flash. Process names longer than 6 characters would not fit `processName[7]`. + +## Runtime behaviour + +1. `setup()`: Serial 115200, **watchdogs disabled**, I²C + LCD init, custom thermometer glyph, motor disabled, splash `AUTOFILM` + `V0.1.1 20240701` for 1 s. +2. `loop()` calls `startingMenu()` and never returns to a real idle loop while a programme is selected. +3. `startingMenu()` draws 1–6 and **busy-waits** until `devPgm != ""`. +4. `startDev()` walks `cycles`. If `run == 1` it chains `startProcessing()` for the next step (auto-advance after a completed step). Otherwise it shows Step / Time / Temp and waits for U/D/E/X/L/R. +5. `startProcessing()`: + - Shows step name, duration, preferred temp, `Ent:start Esc:quit`. + - Esc (`X`) here aborts **before** motion and returns 0. + - On Enter, records `processStartTime` / `processTimeMillis`, heap-allocates `MotorTaskParams`, and `xTaskCreatePinnedToCore(runMotorTask, …, core 0)`. + - Then **blocks** in `while (millis() < end) { update remaining; delay(410); readTemperature(); }`. + - Deletes the motor task, drives EN HIGH, plays a **blocking 10-beep alarm** (~7.5 s), frees params. + +`runMotorTask` enables the driver and loops forever: CW `cwRotations` revs, then CCW `ccwRotations` revs, using `stepper.run()` busy-wait on each move. `processEndTime` is passed in and **never read**. The task does not check a stop flag. It only ends when the UI task `vTaskDelete`s it. + +Temperature: `sensors.requestTemperatures()` + `getTempCByIndex(0) + 0.4 °C` offset, written to LCD row 3. Called from the UI wait loops. `updateTempDisplay(void *)` exists as a one-shot task that also talks to the LCD and then deletes itself; it is unused from `startProcessing()`. + +Sound: `tone()` + `delay()` on the caller thread. `playTune()` is unused by the main flow. + +## Concurrency as it stands + +| Thread | Role | Problem | +| --- | --- | --- | +| Arduino `loopTask` | All UI, all input, all timing, temp, sound | Blocks for seconds to minutes | +| `MotorTask` (core 0) | Agitation | No cooperative stop; killed from outside | +| Idle 0/1 | — | Removed from TWDT | + +LCD, `stepper`, and `sensors` are used without mutexes. That is safe only because the UI thread does not command the stepper while the motor task runs — except `vTaskDelete` can interrupt `stepper.run()` mid-pulse. + +## Defects and hazards relevant to the refactor + +1. **No stop while a step is running.** Esc is honoured only on the pre-start prompt. The run loop does not read the keypad. This violates the product rule that the machine must always stop on Esc. +2. **`vTaskDelete` of a live motor task** can leave STEP/DIR in an undefined state; EN is dropped only *after* delete. +3. **Watchdog deliberately off** (`include/watchdog.h`). A hung `stepper.run()` or OneWire slot will wedge the chip with no recovery. +4. **Busy-wait input** (`while (true)` + `keypad.getKey()`). `millis() % 1000 == 0` is not a 1 Hz tick; it is a one-millisecond race and can skip or repeat temp reads. +5. **`secondsToMinutesSeconds()` `malloc`s 6 bytes and never frees.** Called from the run loop ~twice per second. +6. **`startDev()` shadows the global** with `int run = 1;` on Enter, so the auto-advance path is inconsistent with the `run` flag used after `startProcessing()` returns. +7. **OneWire conversion blocks the UI** (typically 750 ms at 12-bit). Combined with `delay(410)` the remaining-time display stutters. +8. **Alarm after a step blocks everything** for several seconds; Esc cannot interrupt it. +9. **No input abstraction.** Touch cannot be added without rewriting `menu.cpp`. +10. **Board pins are compile-time literals.** The Guition S3 panel reuses many of these GPIOs for QSPI / GT911; the current map cannot be copied across. + +## What is *not* in the tree + +- ESP-IDF project or dual-firmware CI +- Touch / colour LCD support +- Pause / resume of a live step +- Persistent profiles (README already lists this as future) +- Pumps, valves, reservoirs, drain actuators +- Closed-loop heater control +- Unit tests beyond PlatformIO’s empty `test/README` + +## README vs code + +`README.MD` matches the hardware story (ESP32, NEMA-17, DS18B20, 2004 I²C, Kindermann tank) and the pour-start-drain loop. It still talks about opening the project in Arduino IDE / PlatformIO and does not mention FreeRTOS, the S3 panel, or stop-during-run. diff --git a/docs/TARGET_ARCHITECTURE.md b/docs/TARGET_ARCHITECTURE.md new file mode 100644 index 0000000..c0471b8 --- /dev/null +++ b/docs/TARGET_ARCHITECTURE.md @@ -0,0 +1,303 @@ +# AutoFilm-ESP32 — Target Architecture + +Status: proposed on `refactor/esp-idf-modular-ui`, for review. +Does not change default process times, temperatures, or rotation patterns. + +## Goals + +1. Rebuild as an **ESP-IDF** project that still links **Arduino-as-component** so AccelStepper, Keypad, LiquidCrystal_I2C, OneWire, and DallasTemperature keep working in the first cut. +2. Produce **two firmware images in CI**: classic ESP32-WROOM (2004 + keypad) and Guition **JC4827W543C** (ESP32-S3-WROOM-1-N4R8, NV3041A 480×272 QSPI, GT911 capacitive touch). +3. Split **UI / input** from **mechatronics** so a touch panel can replace the character LCD without rewriting agitation or recipes. +4. Use FreeRTOS tasks, queues, and interrupts so the UI never blocks on motor moves, OneWire conversions, or tones. +5. **Always stop motion on Esc** (keypad position 4 across / 4 down, key `'X'`) and, later, on a touch STOP control. Same command path for both. +6. Keep Arduino libraries isolated behind HAL files; replacing them with native IDF drivers is planned debt, not this document’s implementation phase. +7. Leave room for pumps, valves, reservoirs, and drain without breaking the command/event contract. + +Arduino-as-component is **explicit technical debt**. New modules must not add Arduino types to public headers. + +## Non-goals (this refactor) + +- Changing C41 / E6 / ECN-2 / B&W / Custom / B&WREV default numbers. +- A real Custom-programme editor (session ±5 s only; NVS profiles later). +- Closed-loop bath heating. +- Implementing pumps/valves now — only the extension points. + +## Dual board model + +| | `board_wroom` | `board_jc4827w543` | +| --- | --- | --- | +| SoC | ESP32-WROOM (classic) | ESP32-S3-WROOM-1-N4R8 (4 MB flash, 8 MB octal PSRAM) | +| UI | 20×4 I²C LCD `0x27` + 5×4 keypad | 4.3" IPS 480×272 NV3041A QSPI + GT911 I²C | +| Touch | none | GT911, typical pins SDA 8, SCL 4, INT 3, RST 38, addr `0x5D` | +| Display bus | I²C 21/22 | QSPI CS 45, SCK 47, D0–D3 21/48/40/39, backlight GPIO 1 | +| Mechatronics | current pin map | **remapped**; panel consumes most GPIOs, ~10 usable extras + headers | + +Pin maps live only in board components. Application code sees `hal_motor_enable()`, not `GPIO_NUM_27`. + +CI builds: + +``` +idf.py -D AUTOFILM_BOARD=wroom @sdkconfig.defaults @sdkconfig.wroom +idf.py -D AUTOFILM_BOARD=jc4827w543 @sdkconfig.defaults @sdkconfig.s3 +``` + +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. + +## Proposed tree + +Existing `src/` and `include/` stay until each file is moved. New code lands here: + +``` +AGENTS.md +docs/ + CURRENT_STATE.md + TARGET_ARCHITECTURE.md +main/ # idf app: NVS, task spawn, board select +components/ + app_process/ # recipe table + lookup + session time overrides + app_machine/ # process state machine (no display types) + app_ui/ # screens + command emission (no stepper types) + ui_cmd/ # command / event structs only + hal_motor/ + hal_temp/ + hal_input/ # keypad + (later) GT911 → ui_event + hal_display/ # 2004 adapter; later NV3041A adapter + hal_audio/ + board_wroom/ + board_jc4827w543/ +managed_components/ # Arduino-esp32 component, IDF drivers +``` + +Public headers of `app_*` and `ui_cmd` are C (or `extern "C"`) and Arduino-free. + +## Command / event contract + +UI never starts the motor. UI posts commands. Machine never writes pixels. Machine posts events. + +### Commands (`ui → machine`) + +| Command | Now | Later | +| --- | --- | --- | +| `SelectProcess(id)` | keypad 1–6 | touch tile | +| `BrowseStep(delta)` | U/D | swipe / list | +| `AdjustStepTime(step, delta_s)` | L/R ±5 | stepper buttons | +| `ArmStep(step)` | show Ent/Esc prompt | confirm sheet | +| `StartStep(step)` | Enter | START | +| `Stop` | Esc during run | STOP | +| `Resume` | after Stop | RESUME | +| `ReturnToStepSelect` | after Stop | BACK | +| `CancelArmed` | Esc before start | cancel | + +Reserved, not implemented: + +`PumpSet`, `ValveSet`, `ReservoirSelect`, `DrainStart`, `DrainStop`, `HeaterSetpoint`. + +Adding an actuator is a new command + HAL + machine state. It must not require `app_ui` to include motor headers. + +### Events (`machine → ui`) + +| Event | Payload | +| --- | --- | +| `ProcessSelected` | process id, name, step count | +| `StepView` | index, name, time_s, temp pref/min/max, cw, ccw | +| `StepArmed` | same | +| `StepStarted` | start tick, duration_ms | +| `StepProgress` | remaining_ms, temp_c, motion {cw\|ccw\|idle} | +| `StepComplete` | index | +| `StepStopped` | remaining_ms, index | +| `StepResumed` | remaining_ms | +| `ProcessIdle` | — | +| `TempUpdated` | °C or disconnected | +| `Fault` | code, message | + +Required on-screen fields while running: **temperature, step name, time remaining**. Motion direction is optional. STOP / RESUME / BACK are controls, not telemetry. + +### Transport + +One FreeRTOS queue each way (or a tiny pub/sub wrapping `xQueueSend`). Bounded structs, no `String`. ISR context may only `xQueueSendFromISR` a `Stop` or raw key/touch sample. + +``` +┌────────────┐ cmd_q ┌──────────────┐ actuator calls ┌──────────┐ +│ app_ui │ ─────────► │ app_machine │ ─────────────────► │ hal_* │ +│ + hal_in │ ◄───────── │ + app_process│ ◄── temp / done │ │ +└────────────┘ evt_q └──────────────┘ └──────────┘ +``` + +## State machine + +``` +Idle → ProcessSelected → StepSelect ⇄ StepArmed + │ + ▼ + StepRunning ◄── Resume + │ Stop + ▼ + StepStopped → StepSelect + │ timer done + ▼ + StepComplete → StepSelect (or auto-advance policy later) +``` + +`run == 1` auto-chaining from the current `startDev()` is preserved as a **machine policy flag** (`auto_advance`), not as UI blocking. Default: after `StepComplete`, arm the next step and wait for Start (safer with pour/drain). If we must match today’s “immediately start the next step after the alarm”, that is one flag, not a second code path. + +### Stop / resume (product rule) + +- **Stop** = immediately end the agitation routine, **disable the motor driver**, keep the step and remaining time, show Resume or Return to step select. +- **Resume** = continue the **same step** for the remaining time with the same CW/CCW pattern. Short beep. +- **Return** = leave the step stopped, motor disabled, back to step list. Remaining time for that step stays at whatever was left (session override), unless we later decide to snap back to nominal — default is keep remaining. +- Esc **before** Start stays “cancel arm”, not Stop. +- Short beep on Stop and on Resume. The current 10-beep end-of-step alarm becomes a non-blocking audio request on `StepComplete`. + +Esc must work in `StepRunning`, `StepStopped` (as Back), and during the complete-alarm. + +## Tasks, cores, interrupts + +Watchdogs stay **on**. Every long-lived task calls `esp_task_wdt_reset()` or is not on the TWDT. + +| Task | Priority (relative) | Work | +| --- | --- | --- | +| `input_task` | high | Keypad scan ~20–50 Hz; GT911 read on INT or poll. Debounce. Map to commands. | +| `ui_task` | mid | Consume events, draw dirty regions only. Never `delay` for motor or OneWire. | +| `machine_task` | mid-high | State machine, timers via `vTaskDelayUntil` / `esp_timer`. | +| `motor_task` | high | Agitation pattern. Waits on notifications, not `vTaskDelete`. | +| `temp_task` | low | DS18B20: kick conversion, wait conversion time, publish `TempUpdated`. | +| `audio_task` | low | Short beeps / alarm pattern from a queue. | + +Arduino `loop()` becomes a thin idle (or is not used). `setup()` only starts IDF tasks. + +### Stop path (must be low latency) + +1. Keypad Esc or GT911 hit-test on STOP is recognized in `input_task` or, for the matrix Esc line if we later wire a dedicated GPIO, in a GPIO ISR. +2. `Stop` is queued to `machine_task` **and** `hal_motor_request_stop()` runs: + - set an atomic `stop_req` + - drive **EN HIGH immediately** (driver off) + - `xTaskNotify` the motor task to abandon the current move +3. Motor task leaves `stepper.run()` at the next loop check (or aborts the RMT transaction in the IDF driver) and parks. +4. Machine publishes `StepStopped` with remaining time. UI shows Resume / Back. + +EN-off is the safety action. It does not need a new motor-driver IC. + +### Question 8, resolved: RMT / MCPWM vs AccelStepper + +No extra power electronics. The existing STEP/DIR driver stays. + +Today AccelStepper **busy-polls** `stepper.run()` on a CPU core to generate step edges. That works, but: + +- Stop latency is “next poll of `distanceToGo`”, and `vTaskDelete` is unsafe mid-pulse. +- The core cannot sleep; jitter depends on competing work. + +ESP32 **RMT** (or MCPWM + a step counter) can emit the pulse train in hardware. The CPU only queues “N steps this direction”. Stop then is: abort the RMT TX + drop EN, which is cleaner. + +**Phase 1:** keep AccelStepper behind `hal_motor`, but add `stop_req` checks inside the inner `run()` loop and never `vTaskDelete` the task. +**Phase 2 (debt burn-down):** replace AccelStepper with an IDF RMT stepper. Same `hal_motor` API. + +## HAL sketches + +```c +void hal_motor_init(void); +void hal_motor_enable(bool on); +void hal_motor_request_stop(void); // ISR-safe: EN off + flag +esp_err_t hal_motor_move_revs(float revs, int dir, uint32_t rpm); +bool hal_motor_is_busy(void); + +void hal_temp_init(void); +esp_err_t hal_temp_read_c(float *out); // blocking OK — temp_task only + +typedef enum { UI_KEY, UI_TOUCH } ui_src_t; +void hal_input_init(void); +bool hal_input_pop(ui_raw_event_t *ev); + +void hal_display_init(void); +void hal_display_clear(void); +void hal_display_text(int col, int row, const char *s); // 2004 mapping +void hal_display_flush(void); // no-op on 2004 + +void hal_audio_beep_short(void); +void hal_audio_alarm_complete(void); +``` + +Colour UI implements a richer draw API *or* LVGL flush, still inside `hal_display` / `app_ui`, never inside `app_machine`. + +## UI strategy: 2004 now, touch without a rewrite + +`app_ui` is a set of **screens** (`ProgramSelect`, `StepSelect`, `Armed`, `Running`, `Stopped`) that read the last event snapshot and emit commands. + +- **WROOM adapter:** screens print 20×4 strings. Input adapter translates `'1'`…`'X'` into commands. +- **S3 adapter:** same screens, different renderer. + +### LVGL vs custom (recommendation) + +The JC4827W543 community stack is Arduino_GFX + LVGL + TAMC_GT911 / TouchLib. LVGL will render a pretty 480×272 panel and is already demoed on this exact board. + +For AutoFilm the visible surface is small: six programmes, a step list, one run view (name / remaining / temp), Stop / Resume / Back. That is a few dirty-rectangle updates per second. + +**Recommendation: custom screens first, LVGL optional later.** + +- Custom keeps the UI task tiny, avoids 4 MB-flash + PSRAM fight with LVGL v8/v9 buffers, and does not force Arduino_GFX deeper into the tree. +- LVGL widgets are worth it when we have graphs, profile editors, or multi-language layouts. Those are out of scope. +- If a first S3 prototype is faster with vendor LVGL demos, isolate LVGL in `board_jc4827w543` + `app_ui` and **do not** put `lv_*` types in `ui_cmd` or `app_machine`. +- Either way: LVGL `flush_cb` and touch read run on `ui_task` / `input_task` only; no `lv_timer_handler()` on `machine_task`. + +Touch STOP is a hit-test that emits the same `Stop` command as Esc. + +## Process data + +Keep the six recipes and their numeric defaults. The storage shape may change. + +Proposed: + +```c +typedef struct { + const char *name; + uint8_t step_count; + const process_step_t *steps; /* flash / const */ +} process_def_t; + +typedef struct { + const char *name; + uint16_t time_s; + float cw_revs; + float ccw_revs; + float temp_min_c; + float temp_pref_c; + float temp_max_c; +} process_step_t; +``` + +Session overlay: `int16_t time_delta_s[step]` in RAM (L/R ±5). Not persisted. NVS profile slot is a later increment behind `app_process` (`process_save_custom()`, etc.). Custom remains the 4×10 s stub. + +`processName[7]` becomes a pointer; `"B&WREV"` no longer depends on a 6-char cap. + +## Audio + +`tone()` + `delay()` moves into `audio_task`. Machine sends `AUDIO_BEEP_SHORT` or `AUDIO_ALARM_COMPLETE`. Stop must pre-empt an in-progress alarm. + +## Safety and timing + +- Motor EN is safe-state HIGH (disabled) at boot and on any `Fault` or `Stop`. +- Step deadline is `esp_timer` one-shot, not a `millis()` busy loop. +- Temp offset `+0.4 °C` stays a board/config constant. +- Re-enable TWDT; conversion wait lives in `temp_task`. +- No `vTaskDelete` of worker tasks after init. Tasks block on queues/notifications for life. + +## 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. + +## Acceptance checks for the first behavioural cut (WROOM) + +- Defaults for all six processes match `docs/CURRENT_STATE.md` tables. +- Esc during a running step disables the motor within one step-pulse loop iteration and shows Resume / Back. +- Resume continues remaining time; Back returns to step select. +- Short beep on Stop and Resume; complete-alarm does not block Esc. +- Temperature updates while running without stalling remaining-time display. +- UI remains navigable if the DS18B20 is disconnected (`--` as today). +- Watchdog no longer disabled as a matter of policy.