8.6 KiB
AutoFilm-ESP32 — Current State
Snapshot of the pre-refactor PlatformIO Arduino tree (9186d84).
Integration going forward is develop; this refactor lands via PR from refactor/esp-idf-modular-ui into develop.
This document describes the tree as it existed at that snapshot, 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 stepperLiquidCrystal_I2C— 20×4 character LCDKeypad— matrix scanOneWire+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).
Note — S3 panel (JC4827W543C) pin lock via board_jc4827w543 accessors: motor EN 7 / STEP 16 / DIR 15 on header P3 (IO6/7/15/16), DS18B20 on 17 on header P4 (GND/3V3/17/18, UART1 unused), speaker amp I2S BCLK 42 / LRCLK 2 / DIN 41 (P7 Speak). Header P2 carries IO46/9/14/5 and is left unused (IO46 is input-only).
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 stepsprocessTime[20]— secondsprocessCycleName[20][10]processCycle[2][20]—[0]CW revolutions,[1]CCW revolutionsprocessTemp[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
setup(): Serial 115200, watchdogs disabled, I²C + LCD init, custom thermometer glyph, motor disabled, splashAUTOFILM+V0.1.1 20240701for 1 s.loop()callsstartingMenu()and never returns to a real idle loop while a programme is selected.startingMenu()draws 1–6 and busy-waits untildevPgm != "".startDev()walkscycles. Ifrun == 1it chainsstartProcessing()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.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-allocatesMotorTaskParams, andxTaskCreatePinnedToCore(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.
- Shows step name, duration, preferred temp,
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 vTaskDeletes 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
- 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.
vTaskDeleteof a live motor task can leave STEP/DIR in an undefined state; EN is dropped only after delete.- Watchdog deliberately off (
include/watchdog.h). A hungstepper.run()or OneWire slot will wedge the chip with no recovery. - Busy-wait input (
while (true)+keypad.getKey()).millis() % 1000 == 0is not a 1 Hz tick; it is a one-millisecond race and can skip or repeat temp reads. secondsToMinutesSeconds()mallocs 6 bytes and never frees. Called from the run loop ~twice per second.startDev()shadows the global withint run = 1;on Enter, so the auto-advance path is inconsistent with therunflag used afterstartProcessing()returns.- OneWire conversion blocks the UI (typically 750 ms at 12-bit). Combined with
delay(410)the remaining-time display stutters. - Alarm after a step blocks everything for several seconds; Esc cannot interrupt it.
- No input abstraction. Touch cannot be added without rewriting
menu.cpp. - 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.