Files
AutoFilm-ESP32/docs/megaplans/refactor/P03-machine-host.md
gronod 8f5b41ebba
ci / test (pull_request) Failing after 12s
ci / firmware (jc4827w543, sdkconfig.s3, esp32s3) (pull_request) Skipped
ci / firmware (wroom, sdkconfig.wroom, esp32) (pull_request) Skipped
ci / test (push) Failing after 12s
ci / firmware (jc4827w543, sdkconfig.s3, esp32s3) (push) Skipped
ci / firmware (wroom, sdkconfig.wroom, esp32) (push) Skipped
Add host tests for machine stop resume and complete
2026-09-16 11:58:06 +00:00

137 lines
5.7 KiB
Markdown

# P03 — app_machine + stub HAL + state tests
STATUS: DONE
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
- [x] STOP path calls `hal_motor_request_stop` before any other work
- [x] default auto_advance false
- [x] host tests cover list above
- [x] STATUS→DONE