From 9b2c6bf3eef856c12e1c01c38d127a8ffe9b05be Mon Sep 17 00:00:00 2001 From: gronod Date: Wed, 16 Sep 2026 11:43:12 +0000 Subject: [PATCH 1/4] Mark P05 BLOCKED pending P03 HAL and app_machine P05 requires landed app_machine + hal_* headers. Those files are absent and P03 is still TODO. Record the block; do not invent public APIs. --- docs/megaplans/REFACTOR-MEGAPLAN.md | 4 +++- docs/megaplans/refactor/P05-hal-wroom-motion.md | 14 +++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/megaplans/REFACTOR-MEGAPLAN.md b/docs/megaplans/REFACTOR-MEGAPLAN.md index c36282b..e5d135e 100644 --- a/docs/megaplans/REFACTOR-MEGAPLAN.md +++ b/docs/megaplans/REFACTOR-MEGAPLAN.md @@ -37,13 +37,15 @@ If blocked: stop, commit nothing broken, write `BLOCKED:` at top of the phase fi | P02 | `refactor/P02-ui-cmd-process.md` | `ui_cmd` + `app_process` + golden tests | DONE | | P03 | `refactor/P03-machine-host.md` | `app_machine` + stub HAL + SM tests | TODO | | P04 | `refactor/P04-gitea-ci.md` | `.gitea/workflows/ci.yml` live | DONE | -| P05 | `refactor/P05-hal-wroom-motion.md` | WROOM motor/temp/audio HAL | TODO | +| P05 | `refactor/P05-hal-wroom-motion.md` | WROOM motor/temp/audio HAL | BLOCKED | | P06 | `refactor/P06-ui-wroom-cutover.md` | 2004 UI + keypad + Stop/Resume cutover | TODO | | P07 | `refactor/P07-board-s3-ui.md` | JC4827W543 display+GT911+STOP | TODO | | P08 | `refactor/P08-idf-debt.md` | Replace Arduino drivers (RMT etc.) | TODO | P04 Notes: merge_bin uses `${{ gitea.sha }}` (not `github.sha`); runner unverified (no Actions run). +P05 Notes: BLOCKED — no app_machine + HAL headers from P03 (branch @ 415c13a). Not implemented. + ## Dependency ``` diff --git a/docs/megaplans/refactor/P05-hal-wroom-motion.md b/docs/megaplans/refactor/P05-hal-wroom-motion.md index f32c901..581a707 100644 --- a/docs/megaplans/refactor/P05-hal-wroom-motion.md +++ b/docs/megaplans/refactor/P05-hal-wroom-motion.md @@ -1,6 +1,18 @@ +BLOCKED: P03 artifacts missing on branch refactor/esp-idf-modular-ui @ 415c13a. + +Required and absent: +- components/app_machine/ (include/app_machine.h, app_machine.c) +- components/hal_motor/include/hal_motor.h +- components/hal_temp/include/hal_temp.h +- components/hal_audio/include/hal_audio.h + +Megaplan table still lists P03 STATUS=TODO. Protocol: P05 requires P03 APIs. +Do not invent or change public HAL/app_machine APIs. Do not implement P01–P04 in this session. +No device HAL, tasks, Arduino-as-component, or main.c cutover landed. + # P05 — WROOM HAL motor / temp / audio -STATUS: TODO +STATUS: BLOCKED DEPENDS: P03 READ: this file, `include/config.h`, `src/motor.cpp`, `src/temperature.cpp`, `src/sound.cpp`, `components/hal_*/include/*.h` OUT: rewriting `app_ui` / deleting menus; S3 motor pins; disabling WDT; `vTaskDelete` motor -- 2.39.5 From 461771cc020a705f94de1618c7b11123dd9f9b23 Mon Sep 17 00:00:00 2001 From: gronod Date: Wed, 16 Sep 2026 11:57:22 +0000 Subject: [PATCH 2/4] Add HAL headers and host stubs for motor temp audio --- components/hal_audio/include/hal_audio.h | 6 +++ components/hal_motor/include/hal_motor.h | 16 +++++++ components/hal_temp/include/hal_temp.h | 10 +++++ tests/host/esp_host_compat.h | 1 + tests/host/stubs/hal_audio.c | 27 ++++++++++++ tests/host/stubs/hal_motor.c | 55 ++++++++++++++++++++++++ tests/host/stubs/hal_temp.c | 22 ++++++++++ 7 files changed, 137 insertions(+) create mode 100644 components/hal_audio/include/hal_audio.h create mode 100644 components/hal_motor/include/hal_motor.h create mode 100644 components/hal_temp/include/hal_temp.h create mode 100644 tests/host/stubs/hal_audio.c create mode 100644 tests/host/stubs/hal_motor.c create mode 100644 tests/host/stubs/hal_temp.c diff --git a/components/hal_audio/include/hal_audio.h b/components/hal_audio/include/hal_audio.h new file mode 100644 index 0000000..0e0f9f0 --- /dev/null +++ b/components/hal_audio/include/hal_audio.h @@ -0,0 +1,6 @@ +#pragma once + +void hal_audio_init(void); +void hal_audio_beep_short(void); +void hal_audio_alarm_complete(void); +void hal_audio_alarm_cancel(void); diff --git a/components/hal_motor/include/hal_motor.h b/components/hal_motor/include/hal_motor.h new file mode 100644 index 0000000..82e794c --- /dev/null +++ b/components/hal_motor/include/hal_motor.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include +#ifdef AUTOFILM_HOST +#include "esp_host_compat.h" +#else +#include "esp_err.h" +#endif + +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); diff --git a/components/hal_temp/include/hal_temp.h b/components/hal_temp/include/hal_temp.h new file mode 100644 index 0000000..5936405 --- /dev/null +++ b/components/hal_temp/include/hal_temp.h @@ -0,0 +1,10 @@ +#pragma once + +#ifdef AUTOFILM_HOST +#include "esp_host_compat.h" +#else +#include "esp_err.h" +#endif + +void hal_temp_init(void); +esp_err_t hal_temp_read_c(float *out); /* ESP_FAIL → disconnected */ diff --git a/tests/host/esp_host_compat.h b/tests/host/esp_host_compat.h index e8abb07..d866576 100644 --- a/tests/host/esp_host_compat.h +++ b/tests/host/esp_host_compat.h @@ -2,5 +2,6 @@ typedef int esp_err_t; #define ESP_OK 0 +#define ESP_FAIL -1 #define ESP_ERR_NOT_FOUND 0x105 #define ESP_ERR_INVALID_ARG 0x102 diff --git a/tests/host/stubs/hal_audio.c b/tests/host/stubs/hal_audio.c new file mode 100644 index 0000000..87ebf0a --- /dev/null +++ b/tests/host/stubs/hal_audio.c @@ -0,0 +1,27 @@ +#include "hal_audio.h" + +int stub_beep_count; +int stub_alarm_count; +int stub_alarm_cancel_count; + +void hal_audio_init(void) +{ + stub_beep_count = 0; + stub_alarm_count = 0; + stub_alarm_cancel_count = 0; +} + +void hal_audio_beep_short(void) +{ + stub_beep_count++; +} + +void hal_audio_alarm_complete(void) +{ + stub_alarm_count++; +} + +void hal_audio_alarm_cancel(void) +{ + stub_alarm_cancel_count++; +} diff --git a/tests/host/stubs/hal_motor.c b/tests/host/stubs/hal_motor.c new file mode 100644 index 0000000..1c00b2f --- /dev/null +++ b/tests/host/stubs/hal_motor.c @@ -0,0 +1,55 @@ +#include "hal_motor.h" + +bool stub_motor_enabled; +int stub_stop_count; +int stub_agitate_start_count; +int stub_agitate_stop_count; +int stub_enable_count; +float stub_last_cw; +float stub_last_ccw; +uint32_t stub_last_rpm; + +void hal_motor_init(void) +{ + stub_motor_enabled = false; + stub_stop_count = 0; + stub_agitate_start_count = 0; + stub_agitate_stop_count = 0; + stub_enable_count = 0; + stub_last_cw = 0.f; + stub_last_ccw = 0.f; + stub_last_rpm = 0; +} + +void hal_motor_enable(bool on) +{ + stub_enable_count++; + stub_motor_enabled = on; +} + +void hal_motor_request_stop(void) +{ + stub_stop_count++; + stub_motor_enabled = false; +} + +esp_err_t hal_motor_agitate_start(float cw_revs, float ccw_revs, uint32_t rpm) +{ + stub_agitate_start_count++; + stub_last_cw = cw_revs; + stub_last_ccw = ccw_revs; + stub_last_rpm = rpm; + stub_motor_enabled = true; + return ESP_OK; +} + +void hal_motor_agitate_stop(void) +{ + stub_agitate_stop_count++; + stub_motor_enabled = false; +} + +bool hal_motor_is_enabled(void) +{ + return stub_motor_enabled; +} diff --git a/tests/host/stubs/hal_temp.c b/tests/host/stubs/hal_temp.c new file mode 100644 index 0000000..2780e96 --- /dev/null +++ b/tests/host/stubs/hal_temp.c @@ -0,0 +1,22 @@ +#include "hal_temp.h" +#include + +int stub_temp_fail; +float stub_temp_c = 20.0f; + +void hal_temp_init(void) +{ + stub_temp_fail = 0; + stub_temp_c = 20.0f; +} + +esp_err_t hal_temp_read_c(float *out) +{ + if (stub_temp_fail) { + return ESP_FAIL; + } + if (out != NULL) { + *out = stub_temp_c; + } + return ESP_OK; +} -- 2.39.5 From c047d5d96f042af0e478f29105d643f47a245527 Mon Sep 17 00:00:00 2001 From: gronod Date: Wed, 16 Sep 2026 11:57:45 +0000 Subject: [PATCH 3/4] Add app_machine state machine with stop and resume --- components/app_machine/CMakeLists.txt | 3 + components/app_machine/app_machine.c | 425 +++++++++++++++++++ components/app_machine/include/app_machine.h | 24 ++ 3 files changed, 452 insertions(+) create mode 100644 components/app_machine/CMakeLists.txt create mode 100644 components/app_machine/app_machine.c create mode 100644 components/app_machine/include/app_machine.h diff --git a/components/app_machine/CMakeLists.txt b/components/app_machine/CMakeLists.txt new file mode 100644 index 0000000..3f25a7d --- /dev/null +++ b/components/app_machine/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "app_machine.c" + INCLUDE_DIRS "include" + REQUIRES ui_cmd app_process) diff --git a/components/app_machine/app_machine.c b/components/app_machine/app_machine.c new file mode 100644 index 0000000..e2fd6e2 --- /dev/null +++ b/components/app_machine/app_machine.c @@ -0,0 +1,425 @@ +#include "app_machine.h" +#include "app_process.h" +#include "hal_motor.h" +#include "hal_temp.h" +#include "hal_audio.h" + +#include + +#define AGITATE_RPM 60u + +static machine_state_t s_state; +static uint8_t s_proc; +static uint8_t s_step; +static uint32_t s_remaining_ms; +static uint32_t s_deadline_ms; +static bool s_have_deadline; +static bool s_resume_pending; +static bool s_auto_advance; +static ui_evt_t s_q[UI_EVT_QUEUE_LEN]; +static uint8_t s_q_head; +static uint8_t s_q_tail; +static uint8_t s_q_count; + +static void emit(ui_evt_t ev) +{ + if (s_q_count == UI_EVT_QUEUE_LEN) { + s_q_head = (uint8_t)((s_q_head + 1u) % UI_EVT_QUEUE_LEN); + s_q_count--; + } + s_q[s_q_tail] = ev; + s_q_tail = (uint8_t)((s_q_tail + 1u) % UI_EVT_QUEUE_LEN); + s_q_count++; +} + +static void fill_common(ui_evt_t *ev) +{ + const process_def_t *p = app_process_get(s_proc); + memset(ev, 0, sizeof(*ev)); + ev->process_id = s_proc; + ev->step_index = s_step; + ev->step_count = (p != NULL) ? p->step_count : 0; + ev->time_s = app_process_time_s(s_proc, s_step); + ev->remaining_ms = s_remaining_ms; + if (p != NULL && s_step < p->step_count) { + ev->cw_revs = p->steps[s_step].cw_revs; + ev->ccw_revs = p->steps[s_step].ccw_revs; + ev->temp_min_c = p->steps[s_step].temp_min_c; + ev->temp_pref_c = p->steps[s_step].temp_pref_c; + ev->temp_max_c = p->steps[s_step].temp_max_c; + } +} + +static void emit_id(ui_evt_id_t id) +{ + ui_evt_t ev; + fill_common(&ev); + ev.id = id; + emit(ev); +} + +static void emit_fault(int16_t code) +{ + ui_evt_t ev; + fill_common(&ev); + ev.id = EVT_FAULT; + ev.fault_code = code; + emit(ev); +} + +static void apply_step_index(uint8_t step) +{ + const process_def_t *p = app_process_get(s_proc); + if (p == NULL || p->step_count == 0) { + return; + } + if (step >= p->step_count) { + step = (uint8_t)(p->step_count - 1u); + } + s_step = step; +} + +static void replace_session_time_s(uint16_t time_s) +{ + uint16_t cur = app_process_time_s(s_proc, s_step); + int32_t left = (int32_t)time_s - (int32_t)cur; + while (left != 0) { + int8_t chunk = (left > 127) ? 127 : (left < -128) ? -128 : (int8_t)left; + if (app_process_adjust_time(s_proc, s_step, chunk) != ESP_OK) { + break; + } + uint16_t now = app_process_time_s(s_proc, s_step); + int32_t next_left = (int32_t)time_s - (int32_t)now; + if (next_left == left) { + break; + } + left = next_left; + } +} + +static void go_step_select(void) +{ + s_state = ST_STEP_SELECT; + s_have_deadline = false; + s_resume_pending = false; + emit_id(EVT_STEP_VIEW); +} + +static void go_idle(void) +{ + s_state = ST_IDLE; + s_have_deadline = false; + s_resume_pending = false; + s_remaining_ms = 0; + emit_id(EVT_PROCESS_IDLE); +} + +static void enter_armed(void) +{ + s_state = ST_ARMED; + s_have_deadline = false; + s_resume_pending = false; + emit_id(EVT_STEP_ARMED); +} + +static void start_running(uint32_t now_ms) +{ + const process_def_t *p = app_process_get(s_proc); + uint16_t t_s = app_process_time_s(s_proc, s_step); + s_remaining_ms = (uint32_t)t_s * 1000u; + s_deadline_ms = now_ms + s_remaining_ms; + s_have_deadline = true; + s_resume_pending = false; + + if (t_s == 0 || p == NULL) { + hal_motor_enable(false); + } else { + (void)hal_motor_agitate_start(p->steps[s_step].cw_revs, + p->steps[s_step].ccw_revs, + AGITATE_RPM); + hal_motor_enable(true); + } + s_state = ST_RUNNING; + emit_id(EVT_STEP_STARTED); +} + +static void complete_step(void) +{ + hal_motor_agitate_stop(); + hal_motor_enable(false); + s_remaining_ms = 0; + s_have_deadline = false; + s_resume_pending = false; + s_state = ST_COMPLETE; + hal_audio_alarm_complete(); + emit_id(EVT_STEP_COMPLETE); +} + +static void maybe_auto_advance(uint32_t now_ms) +{ + (void)now_ms; + if (!s_auto_advance) { + return; + } + const process_def_t *p = app_process_get(s_proc); + if (p == NULL) { + go_idle(); + return; + } + if ((uint16_t)s_step + 1u < p->step_count) { + s_step++; + enter_armed(); + } else { + go_idle(); + } +} + +static void arm_or_next_from_complete(void) +{ + const process_def_t *p = app_process_get(s_proc); + if (p == NULL) { + go_idle(); + return; + } + if ((uint16_t)s_step + 1u < p->step_count) { + s_step++; + enter_armed(); + } else { + go_idle(); + } +} + +void app_machine_init(void) +{ + s_state = ST_IDLE; + s_proc = 0; + s_step = 0; + s_remaining_ms = 0; + s_deadline_ms = 0; + s_have_deadline = false; + s_resume_pending = false; + s_auto_advance = false; + s_q_head = 0; + s_q_tail = 0; + s_q_count = 0; + app_process_init(); + hal_motor_init(); + hal_temp_init(); + hal_audio_init(); +} + +machine_state_t app_machine_state(void) +{ + return s_state; +} + +uint8_t app_machine_process_id(void) +{ + return s_proc; +} + +uint8_t app_machine_step_index(void) +{ + return s_step; +} + +uint32_t app_machine_remaining_ms(void) +{ + return s_remaining_ms; +} + +int app_machine_last_event(ui_evt_t *out) +{ + if (s_q_count == 0 || out == NULL) { + return 0; + } + *out = s_q[s_q_head]; + s_q_head = (uint8_t)((s_q_head + 1u) % UI_EVT_QUEUE_LEN); + s_q_count--; + return 1; +} + +void app_machine_on_temp(float c, bool ok) +{ + ui_evt_t ev; + fill_common(&ev); + ev.id = EVT_TEMP_UPDATED; + ev.temp_c = c; + ev.temp_ok = ok; + emit(ev); +} + +void app_machine_tick(uint32_t now_ms) +{ + if (s_state == ST_RUNNING && s_resume_pending) { + s_deadline_ms = now_ms + s_remaining_ms; + s_have_deadline = true; + s_resume_pending = false; + } + + if (s_state == ST_RUNNING && s_have_deadline) { + if (now_ms >= s_deadline_ms) { + s_remaining_ms = 0; + complete_step(); + maybe_auto_advance(now_ms); + return; + } + s_remaining_ms = s_deadline_ms - now_ms; + emit_id(EVT_STEP_PROGRESS); + } +} + +void app_machine_handle_cmd(const ui_cmd_t *cmd) +{ + if (cmd == NULL) { + return; + } + + switch (cmd->id) { + case CMD_SELECT_PROCESS: + if (app_process_get(cmd->process_id) == NULL) { + emit_fault(1); + return; + } + s_proc = cmd->process_id; + s_step = 0; + s_remaining_ms = 0; + s_state = ST_STEP_SELECT; + emit_id(EVT_PROCESS_SELECTED); + emit_id(EVT_STEP_VIEW); + return; + + case CMD_BROWSE_STEP: + if (s_state != ST_STEP_SELECT) { + return; + } + { + const process_def_t *p = app_process_get(s_proc); + if (p == NULL || p->step_count == 0) { + return; + } + int32_t next = (int32_t)s_step + (int32_t)cmd->step_delta; + if (next < 0) { + next = 0; + } + if (next >= (int32_t)p->step_count) { + next = (int32_t)p->step_count - 1; + } + s_step = (uint8_t)next; + emit_id(EVT_STEP_VIEW); + } + return; + + case CMD_ADJUST_STEP_TIME: + if (s_state != ST_STEP_SELECT) { + return; + } + if (app_process_adjust_time(s_proc, s_step, cmd->time_delta_s) == ESP_OK) { + emit_id(EVT_STEP_VIEW); + } else { + emit_fault(1); + } + return; + + case CMD_ARM_STEP: + if (s_state == ST_STEP_SELECT) { + apply_step_index(cmd->step_index); + enter_armed(); + } else if (s_state == ST_COMPLETE) { + arm_or_next_from_complete(); + } + return; + + case CMD_START_STEP: + if (s_state == ST_STEP_SELECT) { + apply_step_index(cmd->step_index); + enter_armed(); + } else if (s_state == ST_ARMED) { + apply_step_index(cmd->step_index); + start_running(0); + } else if (s_state == ST_COMPLETE) { + arm_or_next_from_complete(); + } + return; + + case CMD_CANCEL_ARMED: + if (s_state == ST_ARMED) { + hal_motor_enable(false); + go_step_select(); + } + return; + + case CMD_STOP: + if (s_state == ST_IDLE) { + return; + } + if (s_state == ST_RUNNING) { + /* request_stop before any other work */ + hal_motor_request_stop(); + hal_motor_enable(false); + if (s_have_deadline && !s_resume_pending) { + /* remaining already updated by last tick; if never ticked, keep start remaining */ + } + hal_audio_beep_short(); + s_state = ST_STOPPED; + s_have_deadline = false; + s_resume_pending = false; + emit_id(EVT_STEP_STOPPED); + return; + } + if (s_state == ST_ARMED) { + hal_motor_enable(false); + go_step_select(); + return; + } + if (s_state == ST_STOPPED) { + { + uint16_t secs = (uint16_t)((s_remaining_ms + 999u) / 1000u); + replace_session_time_s(secs); + } + hal_motor_enable(false); + go_step_select(); + return; + } + if (s_state == ST_COMPLETE) { + hal_audio_alarm_cancel(); + hal_audio_beep_short(); + go_step_select(); + return; + } + return; + + case CMD_RESUME: + if (s_state == ST_STOPPED) { + const process_def_t *p = app_process_get(s_proc); + hal_audio_beep_short(); + s_resume_pending = true; + s_have_deadline = false; + if (p != NULL && s_remaining_ms > 0) { + (void)hal_motor_agitate_start(p->steps[s_step].cw_revs, + p->steps[s_step].ccw_revs, + AGITATE_RPM); + hal_motor_enable(true); + } else { + hal_motor_enable(false); + } + s_state = ST_RUNNING; + emit_id(EVT_STEP_RESUMED); + } + return; + + case CMD_RETURN_TO_STEP_SELECT: + if (s_state == ST_STOPPED) { + uint16_t secs = (uint16_t)((s_remaining_ms + 999u) / 1000u); + replace_session_time_s(secs); + hal_motor_enable(false); + go_step_select(); + } else if (s_state == ST_COMPLETE) { + go_step_select(); + } + return; + + default: + return; + } +} diff --git a/components/app_machine/include/app_machine.h b/components/app_machine/include/app_machine.h new file mode 100644 index 0000000..3743845 --- /dev/null +++ b/components/app_machine/include/app_machine.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include "ui_cmd.h" + +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); -- 2.39.5 From 8f5b41ebba21c419b0604a7cf54dea9a2c3e0057 Mon Sep 17 00:00:00 2001 From: gronod Date: Wed, 16 Sep 2026 11:58:06 +0000 Subject: [PATCH 4/4] Add host tests for machine stop resume and complete --- docs/megaplans/REFACTOR-MEGAPLAN.md | 2 +- docs/megaplans/refactor/P03-machine-host.md | 10 +- tests/host/CMakeLists.txt | 26 +- tests/host/test_machine.c | 299 ++++++++++++++++++++ 4 files changed, 330 insertions(+), 7 deletions(-) create mode 100644 tests/host/test_machine.c diff --git a/docs/megaplans/REFACTOR-MEGAPLAN.md b/docs/megaplans/REFACTOR-MEGAPLAN.md index e5d135e..ecd5b34 100644 --- a/docs/megaplans/REFACTOR-MEGAPLAN.md +++ b/docs/megaplans/REFACTOR-MEGAPLAN.md @@ -35,7 +35,7 @@ If blocked: stop, commit nothing broken, write `BLOCKED:` at top of the phase fi | P00 | `refactor/P00-docs-lock.md` | Architecture docs already on branch | DONE | | P01 | `refactor/P01-idf-skeleton.md` | Dual-board IDF project compiles | BLOCKED | | P02 | `refactor/P02-ui-cmd-process.md` | `ui_cmd` + `app_process` + golden tests | DONE | -| P03 | `refactor/P03-machine-host.md` | `app_machine` + stub HAL + SM tests | TODO | +| P03 | `refactor/P03-machine-host.md` | `app_machine` + stub HAL + SM tests | DONE | | P04 | `refactor/P04-gitea-ci.md` | `.gitea/workflows/ci.yml` live | DONE | | P05 | `refactor/P05-hal-wroom-motion.md` | WROOM motor/temp/audio HAL | BLOCKED | | P06 | `refactor/P06-ui-wroom-cutover.md` | 2004 UI + keypad + Stop/Resume cutover | TODO | diff --git a/docs/megaplans/refactor/P03-machine-host.md b/docs/megaplans/refactor/P03-machine-host.md index b80a336..3288986 100644 --- a/docs/megaplans/refactor/P03-machine-host.md +++ b/docs/megaplans/refactor/P03-machine-host.md @@ -1,6 +1,6 @@ # P03 — app_machine + stub HAL + state tests -STATUS: TODO +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` @@ -130,7 +130,7 @@ Required cases in `test_machine.c` names: ## DoD -- [ ] STOP path calls `hal_motor_request_stop` before any other work -- [ ] default auto_advance false -- [ ] host tests cover list above -- [ ] STATUS→DONE +- [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 diff --git a/tests/host/CMakeLists.txt b/tests/host/CMakeLists.txt index c7e31a0..340c325 100644 --- a/tests/host/CMakeLists.txt +++ b/tests/host/CMakeLists.txt @@ -3,6 +3,16 @@ project(autofilm_host_tests C) enable_testing() +set(HOST_INCLUDES + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../../components/app_process/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../components/app_machine/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../components/ui_cmd/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../components/hal_motor/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../components/hal_temp/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../components/hal_audio/include +) + add_executable(test_process test_process.c main.c @@ -18,5 +28,19 @@ target_include_directories(test_process PRIVATE target_compile_definitions(test_process PRIVATE AUTOFILM_HOST=1) target_compile_features(test_process PRIVATE c_std_11) target_link_libraries(test_process PRIVATE m) - add_test(NAME test_process COMMAND test_process) + +add_executable(test_machine + test_machine.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../components/app_process/app_process.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../components/app_machine/app_machine.c + stubs/hal_motor.c + stubs/hal_temp.c + stubs/hal_audio.c +) + +target_include_directories(test_machine PRIVATE ${HOST_INCLUDES}) +target_compile_definitions(test_machine PRIVATE AUTOFILM_HOST=1) +target_compile_features(test_machine PRIVATE c_std_11) +target_link_libraries(test_machine PRIVATE m) +add_test(NAME test_machine COMMAND test_machine) diff --git a/tests/host/test_machine.c b/tests/host/test_machine.c new file mode 100644 index 0000000..510ea33 --- /dev/null +++ b/tests/host/test_machine.c @@ -0,0 +1,299 @@ +#include "app_machine.h" +#include "app_process.h" +#include "hal_motor.h" + +#include +#include + +extern bool stub_motor_enabled; +extern int stub_stop_count; +extern int stub_agitate_start_count; +extern int stub_beep_count; +extern int stub_alarm_count; +extern int stub_alarm_cancel_count; + +static int g_failures; + +static void fail(const char *msg) +{ + fprintf(stderr, "FAIL: %s\n", msg); + g_failures++; +} + +static void expect_int(int got, int want, const char *msg) +{ + if (got != want) { + fprintf(stderr, "FAIL: %s got=%d want=%d\n", msg, got, want); + g_failures++; + } +} + +static void expect_u32(uint32_t got, uint32_t want, const char *msg) +{ + if (got != want) { + fprintf(stderr, "FAIL: %s got=%u want=%u\n", msg, got, want); + g_failures++; + } +} + +static void drain_events(void) +{ + ui_evt_t ev; + while (app_machine_last_event(&ev) == 1) { + } +} + +static int pop_ids(ui_evt_id_t *ids, int max) +{ + int n = 0; + ui_evt_t ev; + while (n < max && app_machine_last_event(&ev) == 1) { + ids[n++] = ev.id; + } + return n; +} + +static bool has_id(const ui_evt_id_t *ids, int n, ui_evt_id_t id) +{ + for (int i = 0; i < n; i++) { + if (ids[i] == id) { + return true; + } + } + return false; +} + +static ui_cmd_t cmd_select(uint8_t proc) +{ + ui_cmd_t c; + memset(&c, 0, sizeof(c)); + c.id = CMD_SELECT_PROCESS; + c.process_id = proc; + return c; +} + +static ui_cmd_t cmd_arm(uint8_t step) +{ + ui_cmd_t c; + memset(&c, 0, sizeof(c)); + c.id = CMD_ARM_STEP; + c.step_index = step; + return c; +} + +static ui_cmd_t cmd_start(uint8_t step) +{ + ui_cmd_t c; + memset(&c, 0, sizeof(c)); + c.id = CMD_START_STEP; + c.step_index = step; + return c; +} + +static ui_cmd_t cmd_id(ui_cmd_id_t id) +{ + ui_cmd_t c; + memset(&c, 0, sizeof(c)); + c.id = id; + return c; +} + +static void select_c41_step_view(void) +{ + app_machine_init(); + ui_cmd_t c = cmd_select(PROC_C41); + app_machine_handle_cmd(&c); + expect_int((int)app_machine_state(), ST_STEP_SELECT, "select_c41_step_view state"); + expect_int((int)app_machine_process_id(), PROC_C41, "select_c41_step_view proc"); + expect_int((int)app_machine_step_index(), 0, "select_c41_step_view step"); + ui_evt_id_t ids[8]; + int n = pop_ids(ids, 8); + if (!has_id(ids, n, EVT_PROCESS_SELECTED) || !has_id(ids, n, EVT_STEP_VIEW)) { + fail("select_c41_step_view events"); + } +} + +static void adjust_does_not_mutate_const(void) +{ + app_machine_init(); + ui_cmd_t c = cmd_select(PROC_C41); + app_machine_handle_cmd(&c); + ui_cmd_t adj; + memset(&adj, 0, sizeof(adj)); + adj.id = CMD_ADJUST_STEP_TIME; + adj.time_delta_s = 5; + app_machine_handle_cmd(&adj); + const process_def_t *p = app_process_get(PROC_C41); + expect_int((int)p->steps[0].time_s, 180, "adjust_does_not_mutate_const const time"); + expect_int((int)app_process_time_s(PROC_C41, 0), 185, "adjust_does_not_mutate_const overlay"); +} + +static void arm_start_complete_custom_10s(void) +{ + app_machine_init(); + ui_cmd_t c = cmd_select(PROC_CUSTOM); + app_machine_handle_cmd(&c); + ui_cmd_t arm = cmd_arm(0); + app_machine_handle_cmd(&arm); + expect_int((int)app_machine_state(), ST_ARMED, "custom arm state"); + ui_cmd_t start = cmd_start(0); + app_machine_handle_cmd(&start); + expect_int((int)app_machine_state(), ST_RUNNING, "custom start state"); + expect_int(stub_motor_enabled ? 1 : 0, 1, "custom motor on"); + app_machine_tick(0); + expect_int((int)app_machine_state(), ST_RUNNING, "custom tick0"); + app_machine_tick(9999); + expect_int((int)app_machine_state(), ST_RUNNING, "custom tick9999"); + app_machine_tick(10000); + expect_int((int)app_machine_state(), ST_COMPLETE, "custom complete"); + expect_int(stub_motor_enabled ? 1 : 0, 0, "custom motor off complete"); + expect_int(stub_alarm_count >= 1 ? 1 : 0, 1, "custom alarm"); + /* default auto_advance false */ + app_machine_tick(10001); + expect_int((int)app_machine_state(), ST_COMPLETE, "auto_advance stays complete"); +} + +static void stop_disables_motor_and_resume(void) +{ + app_machine_init(); + ui_cmd_t c = cmd_select(PROC_CUSTOM); + app_machine_handle_cmd(&c); + ui_cmd_t arm = cmd_arm(0); + app_machine_handle_cmd(&arm); + ui_cmd_t start = cmd_start(0); + app_machine_handle_cmd(&start); + app_machine_tick(0); + app_machine_tick(1000); + int stops_before = stub_stop_count; + ui_cmd_t stop = cmd_id(CMD_STOP); + app_machine_handle_cmd(&stop); + expect_int((int)app_machine_state(), ST_STOPPED, "stop state"); + expect_int(stub_motor_enabled ? 1 : 0, 0, "stop motor disabled"); + expect_int(stub_stop_count >= stops_before + 1 ? 1 : 0, 1, "stop_count"); + uint32_t rem = app_machine_remaining_ms(); + if (rem < 8000 || rem > 10000) { + fprintf(stderr, "FAIL: remaining after stop got=%u\n", rem); + g_failures++; + } + int beeps = stub_beep_count; + ui_cmd_t resume = cmd_id(CMD_RESUME); + app_machine_handle_cmd(&resume); + expect_int((int)app_machine_state(), ST_RUNNING, "resume state"); + expect_int(stub_beep_count >= beeps + 1 ? 1 : 0, 1, "resume beep"); + app_machine_tick(1000); + app_machine_tick(1000 + rem); + expect_int((int)app_machine_state(), ST_COMPLETE, "resume then complete"); +} + +static void return_from_stopped(void) +{ + app_machine_init(); + ui_cmd_t c = cmd_select(PROC_CUSTOM); + app_machine_handle_cmd(&c); + ui_cmd_t arm = cmd_arm(0); + app_machine_handle_cmd(&arm); + ui_cmd_t start = cmd_start(0); + app_machine_handle_cmd(&start); + app_machine_tick(0); + app_machine_tick(1000); + ui_cmd_t stop = cmd_id(CMD_STOP); + app_machine_handle_cmd(&stop); + uint32_t rem = app_machine_remaining_ms(); + ui_cmd_t ret = cmd_id(CMD_RETURN_TO_STEP_SELECT); + app_machine_handle_cmd(&ret); + expect_int((int)app_machine_state(), ST_STEP_SELECT, "return_from_stopped state"); + uint16_t overlay = app_process_time_s(PROC_CUSTOM, 0); + uint16_t expect_s = (uint16_t)((rem + 999u) / 1000u); + expect_int((int)overlay, (int)expect_s, "return_from_stopped overlay"); + expect_int((int)app_process_get(PROC_CUSTOM)->steps[0].time_s, 10, "const still 10"); +} + +static void stop_during_complete_cancels_alarm(void) +{ + app_machine_init(); + ui_cmd_t c = cmd_select(PROC_CUSTOM); + app_machine_handle_cmd(&c); + ui_cmd_t arm = cmd_arm(0); + app_machine_handle_cmd(&arm); + ui_cmd_t start = cmd_start(0); + app_machine_handle_cmd(&start); + app_machine_tick(0); + app_machine_tick(10000); + expect_int((int)app_machine_state(), ST_COMPLETE, "complete before stop"); + int cancels = stub_alarm_cancel_count; + ui_cmd_t stop = cmd_id(CMD_STOP); + app_machine_handle_cmd(&stop); + expect_int((int)app_machine_state(), ST_STEP_SELECT, "stop from complete"); + expect_int(stub_alarm_cancel_count >= cancels + 1 ? 1 : 0, 1, "alarm cancel"); +} + +static void ecn2_remjet_zero_time(void) +{ + app_machine_init(); + ui_cmd_t c = cmd_select(PROC_ECN2); + app_machine_handle_cmd(&c); + ui_cmd_t browse; + memset(&browse, 0, sizeof(browse)); + browse.id = CMD_BROWSE_STEP; + browse.step_delta = 1; + app_machine_handle_cmd(&browse); + expect_int((int)app_machine_step_index(), 1, "remjet index"); + ui_cmd_t arm = cmd_arm(1); + app_machine_handle_cmd(&arm); + int starts = stub_agitate_start_count; + ui_cmd_t start = cmd_start(1); + app_machine_handle_cmd(&start); + expect_int(stub_motor_enabled ? 1 : 0, 0, "remjet no enable"); + expect_int(stub_agitate_start_count, starts, "remjet no agitate"); + app_machine_tick(0); + expect_int((int)app_machine_state(), ST_COMPLETE, "remjet complete next tick"); +} + +static void stop_ignored_meaningless_in_idle(void) +{ + app_machine_init(); + int stops = stub_stop_count; + int enables = stub_motor_enabled ? 1 : 0; + ui_cmd_t stop = cmd_id(CMD_STOP); + app_machine_handle_cmd(&stop); + expect_int((int)app_machine_state(), ST_IDLE, "idle stays idle"); + expect_int(stub_stop_count, stops, "idle no request_stop required"); + (void)enables; +} + +static void c41_clock(void) +{ + app_machine_init(); + ui_cmd_t c = cmd_select(PROC_C41); + app_machine_handle_cmd(&c); + ui_cmd_t arm = cmd_arm(0); + app_machine_handle_cmd(&arm); + ui_cmd_t start = cmd_start(0); + app_machine_handle_cmd(&start); + app_machine_tick(0); + expect_int((int)app_machine_state(), ST_RUNNING, "c41 tick0"); + app_machine_tick(179999); + expect_int((int)app_machine_state(), ST_RUNNING, "c41 tick179999"); + app_machine_tick(180000); + expect_int((int)app_machine_state(), ST_COMPLETE, "c41 tick180000"); +} + +int main(void) +{ + 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(); + c41_clock(); + + if (g_failures == 0) { + printf("test_machine: all assertions passed\n"); + return 0; + } + fprintf(stderr, "test_machine: %d failure(s)\n", g_failures); + return 1; +} -- 2.39.5