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_viewadjust_does_not_mutate_constarm_start_complete_custom_10sstop_disables_motor_and_resumereturn_from_stoppedstop_during_complete_cancels_alarmecn2_remjet_zero_timestop_ignored_meaningless_in_idle(IDLE+STOP → stay IDLE, no motor calls required)
COMMITS
Add HAL headers and host stubs for motor temp audioAdd app_machine state machine with stop and resumeAdd host tests for machine stop resume and complete
DoD
- STOP path calls
hal_motor_request_stopbefore any other work - default auto_advance false
- host tests cover list above
- STATUS→DONE