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

5.7 KiB

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:

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)

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