diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..fe0468b --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,65 @@ +name: ci + +on: + pull_request: + branches: [develop] + push: + branches: [develop, "refactor/**", "feature/**"] + tags: ["v*"] + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Host unit tests + if: hashFiles('tests/host/CMakeLists.txt') != '' + run: | + cmake -S tests/host -B build/host + cmake --build build/host + ctest --test-dir build/host --output-on-failure + + firmware: + needs: test + runs-on: ubuntu-latest + container: + image: docker.io/espressif/idf:v5.4 + strategy: + fail-fast: false + matrix: + include: + - board: wroom + target: esp32 + sdkconfig: sdkconfig.wroom + - board: jc4827w543 + target: esp32s3 + sdkconfig: sdkconfig.s3 + steps: + - uses: actions/checkout@v4 + - name: Build + if: hashFiles('CMakeLists.txt') != '' + run: | + git config --global --add safe.directory '*' + idf.py -B build/${{ matrix.board }} set-target ${{ matrix.target }} + idf.py -B build/${{ matrix.board }} \ + -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;${{ matrix.sdkconfig }}" \ + -D AUTOFILM_BOARD=${{ matrix.board }} \ + build + idf.py -B build/${{ matrix.board }} size | tee size-${{ matrix.board }}.txt + python $IDF_PATH/components/esptool_py/esptool/esptool.py \ + --chip ${{ matrix.target }} merge_bin \ + -o autofilm-${{ matrix.board }}-${{ gitea.sha }}.bin \ + @build/${{ matrix.board }}/flash_args + - uses: actions/upload-artifact@v3 + if: hashFiles('CMakeLists.txt') != '' + with: + name: firmware-${{ matrix.board }} + path: | + autofilm-${{ matrix.board }}-*.bin + build/${{ matrix.board }}/*.bin + build/${{ matrix.board }}/bootloader/bootloader.bin + build/${{ matrix.board }}/partition_table/partition-table.bin + build/${{ matrix.board }}/flasher_args.json + build/${{ matrix.board }}/sdkconfig + size-${{ matrix.board }}.txt diff --git a/components/app_process/CMakeLists.txt b/components/app_process/CMakeLists.txt new file mode 100644 index 0000000..da2c136 --- /dev/null +++ b/components/app_process/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "app_process.c" + INCLUDE_DIRS "include" + REQUIRES ui_cmd) diff --git a/components/app_process/app_process.c b/components/app_process/app_process.c new file mode 100644 index 0000000..288e8df --- /dev/null +++ b/components/app_process/app_process.c @@ -0,0 +1,148 @@ +#include "app_process.h" + +#include + +static const process_step_t s_c41_steps[] = { + {"Prewarm", 180, 1.f, 1.f, 37.8f, 38.f, 38.2f}, + {"Developer", 195, 5.5f, 5.f, 37.8f, 38.f, 38.2f}, + {"Bleach", 45, 5.5f, 5.f, 32.f, 38.f, 38.2f}, + {"Fix", 180, 5.5f, 5.f, 32.f, 38.f, 38.2f}, + {"Rinse 1", 60, 3.5f, 3.f, 32.f, 38.f, 38.2f}, + {"Rinse 2", 60, 3.5f, 3.f, 32.f, 38.f, 38.2f}, + {"Fin Rinse", 30, 1.f, 1.f, 32.f, 38.f, 38.2f}, +}; + +static const process_step_t s_e6_steps[] = { + {"Preheat", 180, 1.f, 1.f, 37.5f, 38.f, 38.5f}, + {"FirstDev", 360, 5.5f, 5.f, 37.7f, 38.f, 38.3f}, + {"Wash 1", 120, 3.5f, 3.f, 33.0f, 38.f, 38.0f}, + {"Reversal", 120, 5.f, 5.5f, 37.7f, 38.f, 38.3f}, + {"ColorDev", 360, 5.5f, 5.f, 37.0f, 38.f, 39.0f}, + {"PreBleach", 120, 5.5f, 5.5f, 37.f, 38.f, 38.f}, + {"Bleach", 360, 5.5f, 5.5f, 37.5f, 38.f, 38.5f}, + {"Fixer", 240, 5.5f, 5.5f, 37.5f, 38.f, 38.5f}, + {"Wash 2", 120, 3.5f, 3.f, 33.0f, 38.f, 38.5f}, + {"Wash 3", 120, 3.5f, 3.f, 33.0f, 38.f, 38.5f}, + {"Wash 4", 120, 3.5f, 3.f, 33.0f, 38.f, 38.5f}, + {"Fin Rinse", 30, 3.5f, 3.f, 19.0f, 20.f, 21.0f}, +}; + +static const process_step_t s_ecn2_steps[] = { + {"Prebath", 180, 1.f, 1.f, 27.f, 38.f, 38.f}, + {"RemJet", 0, 0.f, 0.f, 0.f, 0.f, 0.f}, + {"Developer", 210, 5.5f, 5.f, 40.8f, 41.f, 41.2f}, + {"Stop Bath", 60, 3.5f, 3.f, 27.f, 38.f, 38.f}, + {"Wash", 180, 3.5f, 3.f, 27.f, 38.f, 38.f}, + {"Bleach", 150, 5.5f, 5.f, 27.f, 38.f, 38.f}, + {"Fixer", 120, 5.5f, 5.f, 27.f, 38.f, 38.f}, + {"Wash 2", 300, 3.5f, 3.f, 27.f, 38.f, 38.f}, + {"Fin Rinse", 120, 1.f, 1.f, 27.f, 38.f, 38.f}, +}; + +static const process_step_t s_bw_steps[] = { + {"Developer", 510, 5.5f, 5.f, 19.f, 20.f, 21.f}, + {"Stop", 30, 3.5f, 3.f, 19.f, 20.f, 21.f}, + {"Fix", 300, 3.5f, 3.f, 19.f, 20.f, 21.f}, + {"Rinse 1", 60, 3.5f, 3.f, 19.f, 20.f, 21.f}, + {"Rinse 2", 90, 3.5f, 3.f, 19.f, 20.f, 21.f}, + {"Rinse 3", 120, 3.5f, 3.f, 19.f, 20.f, 21.f}, + {"Fin Rinse", 30, 1.f, 1.f, 19.f, 20.f, 21.f}, +}; + +static const process_step_t s_custom_steps[] = { + {"Developer", 10, 5.5f, 5.f, 19.f, 20.f, 21.f}, + {"Stop", 10, 3.5f, 3.f, 19.f, 20.f, 21.f}, + {"Fix", 10, 3.5f, 3.f, 19.f, 20.f, 21.f}, + {"Rinse", 10, 3.5f, 3.f, 19.f, 20.f, 21.f}, +}; + +static const process_step_t s_bwrev_steps[] = { + {"FirstDev", 720, 5.5f, 5.f, 19.5f, 20.f, 20.5f}, + {"Wash 1", 300, 3.5f, 3.f, 15.5f, 20.f, 22.5f}, + {"Bleach", 300, 5.5f, 5.f, 19.5f, 20.f, 22.5f}, + {"Wash 2", 60, 3.5f, 3.f, 15.5f, 20.f, 22.5f}, + {"Clearing", 120, 5.5f, 5.f, 19.5f, 20.f, 22.5f}, + {"Wash 3", 60, 3.5f, 3.f, 15.5f, 20.f, 22.5f}, + {"Reversal", 120, 5.5f, 5.f, 19.5f, 20.f, 22.5f}, + {"SecondDev", 360, 5.5f, 5.f, 19.5f, 20.f, 22.5f}, + {"Wash 4", 60, 3.5f, 3.f, 15.5f, 20.f, 22.5f}, + {"Fix", 300, 5.5f, 5.f, 19.5f, 20.f, 22.5f}, + {"Wash 5", 60, 3.5f, 3.f, 15.5f, 20.f, 22.5f}, + {"Fin Rinse", 60, 3.5f, 3.f, 15.5f, 20.f, 22.5f}, +}; + +static const process_def_t s_processes[PROCESS_COUNT] = { + {"C41", 7, s_c41_steps}, + {"E6", 12, s_e6_steps}, + {"ECN-2", 9, s_ecn2_steps}, + {"B&W", 7, s_bw_steps}, + {"Custom", 4, s_custom_steps}, + {"B&WREV", 12, s_bwrev_steps}, +}; + +static int16_t s_time_delta_s[PROCESS_COUNT][PROCESS_MAX_STEPS]; + +void app_process_init(void) +{ + memset(s_time_delta_s, 0, sizeof(s_time_delta_s)); +} + +const process_def_t *app_process_get(uint8_t id) +{ + if (id >= PROCESS_COUNT) { + return NULL; + } + return &s_processes[id]; +} + +const process_def_t *app_process_find_name(const char *name) +{ + if (name == NULL) { + return NULL; + } + for (uint8_t i = 0; i < PROCESS_COUNT; i++) { + if (strcmp(s_processes[i].name, name) == 0) { + return &s_processes[i]; + } + } + return NULL; +} + +uint16_t app_process_time_s(uint8_t id, uint8_t step) +{ + if (id >= PROCESS_COUNT) { + return 0; + } + const process_def_t *p = &s_processes[id]; + if (step >= p->step_count) { + return 0; + } + int32_t t = (int32_t)p->steps[step].time_s + (int32_t)s_time_delta_s[id][step]; + if (t < 0) { + t = 0; + } + if (t > 36000) { + t = 36000; + } + return (uint16_t)t; +} + +esp_err_t app_process_adjust_time(uint8_t id, uint8_t step, int8_t delta_s) +{ + if (id >= PROCESS_COUNT) { + return ESP_ERR_INVALID_ARG; + } + if (step >= s_processes[id].step_count || step >= PROCESS_MAX_STEPS) { + return ESP_ERR_INVALID_ARG; + } + s_time_delta_s[id][step] = (int16_t)(s_time_delta_s[id][step] + delta_s); + return ESP_OK; +} + +void app_process_reset_session(uint8_t id) +{ + if (id >= PROCESS_COUNT) { + return; + } + memset(s_time_delta_s[id], 0, sizeof(s_time_delta_s[id])); +} diff --git a/components/app_process/include/app_process.h b/components/app_process/include/app_process.h new file mode 100644 index 0000000..ed69b94 --- /dev/null +++ b/components/app_process/include/app_process.h @@ -0,0 +1,39 @@ +#pragma once +#include +#ifdef AUTOFILM_HOST +#include "esp_host_compat.h" +#else +#include "esp_err.h" +#endif + +#define PROCESS_COUNT 6 +#define PROCESS_MAX_STEPS 20 + +enum { + PROC_C41 = 0, + PROC_E6 = 1, + PROC_ECN2 = 2, + PROC_BW = 3, + PROC_CUSTOM = 4, + PROC_BWREV = 5 +}; + +typedef struct { + const char *name; + uint16_t time_s; + float cw_revs, ccw_revs; + float temp_min_c, temp_pref_c, temp_max_c; +} process_step_t; + +typedef struct { + const char *name; + uint8_t step_count; + const process_step_t *steps; +} process_def_t; + +void app_process_init(void); /* zero session deltas */ +const process_def_t *app_process_get(uint8_t id); +const process_def_t *app_process_find_name(const char *name); +uint16_t app_process_time_s(uint8_t id, uint8_t step); /* default + session delta, min 0 */ +esp_err_t app_process_adjust_time(uint8_t id, uint8_t step, int8_t delta_s); +void app_process_reset_session(uint8_t id); diff --git a/components/ui_cmd/CMakeLists.txt b/components/ui_cmd/CMakeLists.txt new file mode 100644 index 0000000..6711169 --- /dev/null +++ b/components/ui_cmd/CMakeLists.txt @@ -0,0 +1 @@ +idf_component_register(INCLUDE_DIRS "include") diff --git a/components/ui_cmd/include/ui_cmd.h b/components/ui_cmd/include/ui_cmd.h new file mode 100644 index 0000000..fe3646a --- /dev/null +++ b/components/ui_cmd/include/ui_cmd.h @@ -0,0 +1,63 @@ +#pragma once +#include +#include + +#define UI_CMD_QUEUE_LEN 8 +#define UI_EVT_QUEUE_LEN 16 + +typedef enum { + CMD_SELECT_PROCESS = 1, + CMD_BROWSE_STEP = 2, + CMD_ADJUST_STEP_TIME = 3, + CMD_ARM_STEP = 4, + CMD_START_STEP = 5, + CMD_STOP = 6, + CMD_RESUME = 7, + CMD_RETURN_TO_STEP_SELECT = 8, + CMD_CANCEL_ARMED = 9, + CMD_PUMP_SET = 100, + CMD_VALVE_SET = 101, + CMD_RESERVOIR_SELECT = 102, + CMD_DRAIN_START = 103, + CMD_DRAIN_STOP = 104, + CMD_HEATER_SETPOINT = 105 +} ui_cmd_id_t; + +typedef enum { + EVT_PROCESS_SELECTED = 1, + EVT_STEP_VIEW = 2, + EVT_STEP_ARMED = 3, + EVT_STEP_STARTED = 4, + EVT_STEP_PROGRESS = 5, + EVT_STEP_COMPLETE = 6, + EVT_STEP_STOPPED = 7, + EVT_STEP_RESUMED = 8, + EVT_PROCESS_IDLE = 9, + EVT_TEMP_UPDATED = 10, + EVT_FAULT = 11 +} ui_evt_id_t; + +typedef enum { MOTION_IDLE = 0, MOTION_CW = 1, MOTION_CCW = 2 } motion_t; + +typedef struct { + ui_cmd_id_t id; + uint8_t process_id; + int8_t step_delta; /* BrowseStep */ + int8_t time_delta_s; /* AdjustStepTime, typically ±5 */ + uint8_t step_index; /* Arm/Start */ +} ui_cmd_t; + +typedef struct { + ui_evt_id_t id; + uint8_t process_id; + uint8_t step_index; + uint8_t step_count; + uint16_t time_s; + uint32_t remaining_ms; + float temp_c; /* NAN if disconnected */ + bool temp_ok; + float temp_min_c, temp_pref_c, temp_max_c; + float cw_revs, ccw_revs; + motion_t motion; + int16_t fault_code; +} ui_evt_t; diff --git a/docs/megaplans/REFACTOR-MEGAPLAN.md b/docs/megaplans/REFACTOR-MEGAPLAN.md index 03751ca..c36282b 100644 --- a/docs/megaplans/REFACTOR-MEGAPLAN.md +++ b/docs/megaplans/REFACTOR-MEGAPLAN.md @@ -34,14 +34,16 @@ 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 | TODO | +| 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 | 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 | | 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). + ## Dependency ``` diff --git a/docs/megaplans/refactor/P02-ui-cmd-process.md b/docs/megaplans/refactor/P02-ui-cmd-process.md index 2441874..aaed106 100644 --- a/docs/megaplans/refactor/P02-ui-cmd-process.md +++ b/docs/megaplans/refactor/P02-ui-cmd-process.md @@ -1,6 +1,6 @@ # P02 — ui_cmd + app_process + golden tests -STATUS: TODO +STATUS: DONE DEPENDS: P01 READ: this file, `src/devSequence.cpp` (values only), `docs/megaplans/REFACTOR-MEGAPLAN.md` OUT: FreeRTOS tasks, HAL, display, Arduino, CI yaml, machine states @@ -191,7 +191,7 @@ All golden tests pass. ## DoD -- [ ] Headers Arduino-free -- [ ] Every cell in golden table asserted -- [ ] Overlay does not mutate const steps -- [ ] STATUS→DONE +- [x] Headers Arduino-free +- [x] Every cell in golden table asserted +- [x] Overlay does not mutate const steps +- [x] STATUS→DONE diff --git a/docs/megaplans/refactor/P04-gitea-ci.md b/docs/megaplans/refactor/P04-gitea-ci.md index 497f9a3..1a45413 100644 --- a/docs/megaplans/refactor/P04-gitea-ci.md +++ b/docs/megaplans/refactor/P04-gitea-ci.md @@ -1,6 +1,7 @@ # P04 — Gitea Actions CI -STATUS: TODO +STATUS: DONE +Notes: used ${{ gitea.sha }} in merge_bin output name (CICD example); github.sha not substituted. Runner unverified (no Actions run). No required-check flip (P01 BLOCKED; images not confirmed compiling). Release-attach job skipped. DEPENDS: P01 (required), P02/P03 (tests job becomes real when `tests/host/CMakeLists.txt` exists) READ: this file, `docs/CICD.md` OUT: changing firmware sources; enabling branch protection yourself if you lack repo admin — then note BLOCKED for humans diff --git a/tests/host/CMakeLists.txt b/tests/host/CMakeLists.txt new file mode 100644 index 0000000..c7e31a0 --- /dev/null +++ b/tests/host/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.16) +project(autofilm_host_tests C) + +enable_testing() + +add_executable(test_process + test_process.c + main.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../components/app_process/app_process.c +) + +target_include_directories(test_process PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../../components/app_process/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../components/ui_cmd/include +) + +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) diff --git a/tests/host/esp_host_compat.h b/tests/host/esp_host_compat.h new file mode 100644 index 0000000..e8abb07 --- /dev/null +++ b/tests/host/esp_host_compat.h @@ -0,0 +1,6 @@ +#pragma once + +typedef int esp_err_t; +#define ESP_OK 0 +#define ESP_ERR_NOT_FOUND 0x105 +#define ESP_ERR_INVALID_ARG 0x102 diff --git a/tests/host/main.c b/tests/host/main.c new file mode 100644 index 0000000..4409a47 --- /dev/null +++ b/tests/host/main.c @@ -0,0 +1,6 @@ +int test_process_run(void); + +int main(void) +{ + return test_process_run(); +} diff --git a/tests/host/test_process.c b/tests/host/test_process.c new file mode 100644 index 0000000..e2c61a5 --- /dev/null +++ b/tests/host/test_process.c @@ -0,0 +1,214 @@ +#include "app_process.h" + +#include +#include +#include + +static int g_failures; + +static void fail(const char *msg) +{ + fprintf(stderr, "FAIL: %s\n", msg); + g_failures++; +} + +static void expect_ptr_null(const void *p, const char *msg) +{ + if (p != NULL) { + fail(msg); + } +} + +static void expect_ptr_nonnull(const void *p, const char *msg) +{ + if (p == NULL) { + fail(msg); + } +} + +static void expect_str(const char *got, const char *want, const char *msg) +{ + if (got == NULL || want == NULL || strcmp(got, want) != 0) { + fprintf(stderr, "FAIL: %s got=\"%s\" want=\"%s\"\n", + msg, got ? got : "(null)", want ? want : "(null)"); + g_failures++; + } +} + +static void expect_u8(uint8_t got, uint8_t want, const char *msg) +{ + if (got != want) { + fprintf(stderr, "FAIL: %s got=%u want=%u\n", msg, got, want); + g_failures++; + } +} + +static void expect_u16(uint16_t got, uint16_t want, const char *msg) +{ + if (got != want) { + fprintf(stderr, "FAIL: %s got=%u want=%u\n", msg, got, want); + g_failures++; + } +} + +static void expect_f(float got, float want, const char *msg) +{ + if (fabsf(got - want) >= 1e-4f) { + fprintf(stderr, "FAIL: %s got=%f want=%f\n", msg, (double)got, (double)want); + g_failures++; + } +} + +static void expect_err(esp_err_t got, esp_err_t want, const char *msg) +{ + if (got != want) { + fprintf(stderr, "FAIL: %s got=%d want=%d\n", msg, got, want); + g_failures++; + } +} + +static void check_step(const process_def_t *p, uint8_t i, + const char *name, uint16_t t, + float cw, float ccw, + float tmin, float tpref, float tmax) +{ + char buf[96]; + snprintf(buf, sizeof(buf), "%s[%u].name", p->name, i); + expect_str(p->steps[i].name, name, buf); + snprintf(buf, sizeof(buf), "%s[%u].time_s", p->name, i); + expect_u16(p->steps[i].time_s, t, buf); + snprintf(buf, sizeof(buf), "%s[%u].cw", p->name, i); + expect_f(p->steps[i].cw_revs, cw, buf); + snprintf(buf, sizeof(buf), "%s[%u].ccw", p->name, i); + expect_f(p->steps[i].ccw_revs, ccw, buf); + snprintf(buf, sizeof(buf), "%s[%u].min", p->name, i); + expect_f(p->steps[i].temp_min_c, tmin, buf); + snprintf(buf, sizeof(buf), "%s[%u].pref", p->name, i); + expect_f(p->steps[i].temp_pref_c, tpref, buf); + snprintf(buf, sizeof(buf), "%s[%u].max", p->name, i); + expect_f(p->steps[i].temp_max_c, tmax, buf); +} + +int test_process_run(void) +{ + app_process_init(); + + expect_ptr_null(app_process_get(6), "get(6)"); + expect_ptr_null(app_process_find_name("nope"), "find_name nope"); + expect_ptr_null(app_process_find_name(NULL), "find_name NULL"); + + const process_def_t *c41 = app_process_get(PROC_C41); + const process_def_t *e6 = app_process_get(PROC_E6); + const process_def_t *ecn = app_process_get(PROC_ECN2); + const process_def_t *bw = app_process_get(PROC_BW); + const process_def_t *custom = app_process_get(PROC_CUSTOM); + const process_def_t *bwrev = app_process_get(PROC_BWREV); + + expect_ptr_nonnull(c41, "C41 get"); + expect_ptr_nonnull(e6, "E6 get"); + expect_ptr_nonnull(ecn, "ECN-2 get"); + expect_ptr_nonnull(bw, "B&W get"); + expect_ptr_nonnull(custom, "Custom get"); + expect_ptr_nonnull(bwrev, "B&WREV get"); + + expect_str(c41->name, "C41", "C41 name"); + expect_u8(c41->step_count, 7, "C41 count"); + check_step(c41, 0, "Prewarm", 180, 1.f, 1.f, 37.8f, 38.f, 38.2f); + check_step(c41, 1, "Developer", 195, 5.5f, 5.f, 37.8f, 38.f, 38.2f); + check_step(c41, 2, "Bleach", 45, 5.5f, 5.f, 32.f, 38.f, 38.2f); + check_step(c41, 3, "Fix", 180, 5.5f, 5.f, 32.f, 38.f, 38.2f); + check_step(c41, 4, "Rinse 1", 60, 3.5f, 3.f, 32.f, 38.f, 38.2f); + check_step(c41, 5, "Rinse 2", 60, 3.5f, 3.f, 32.f, 38.f, 38.2f); + check_step(c41, 6, "Fin Rinse", 30, 1.f, 1.f, 32.f, 38.f, 38.2f); + + expect_str(e6->name, "E6", "E6 name"); + expect_u8(e6->step_count, 12, "E6 count"); + check_step(e6, 0, "Preheat", 180, 1.f, 1.f, 37.5f, 38.f, 38.5f); + check_step(e6, 1, "FirstDev", 360, 5.5f, 5.f, 37.7f, 38.f, 38.3f); + check_step(e6, 2, "Wash 1", 120, 3.5f, 3.f, 33.0f, 38.f, 38.0f); + check_step(e6, 3, "Reversal", 120, 5.f, 5.5f, 37.7f, 38.f, 38.3f); + check_step(e6, 4, "ColorDev", 360, 5.5f, 5.f, 37.0f, 38.f, 39.0f); + check_step(e6, 5, "PreBleach", 120, 5.5f, 5.5f, 37.f, 38.f, 38.f); + check_step(e6, 6, "Bleach", 360, 5.5f, 5.5f, 37.5f, 38.f, 38.5f); + check_step(e6, 7, "Fixer", 240, 5.5f, 5.5f, 37.5f, 38.f, 38.5f); + check_step(e6, 8, "Wash 2", 120, 3.5f, 3.f, 33.0f, 38.f, 38.5f); + check_step(e6, 9, "Wash 3", 120, 3.5f, 3.f, 33.0f, 38.f, 38.5f); + check_step(e6, 10, "Wash 4", 120, 3.5f, 3.f, 33.0f, 38.f, 38.5f); + check_step(e6, 11, "Fin Rinse", 30, 3.5f, 3.f, 19.0f, 20.f, 21.0f); + + expect_str(ecn->name, "ECN-2", "ECN-2 name"); + expect_u8(ecn->step_count, 9, "ECN-2 count"); + check_step(ecn, 0, "Prebath", 180, 1.f, 1.f, 27.f, 38.f, 38.f); + check_step(ecn, 1, "RemJet", 0, 0.f, 0.f, 0.f, 0.f, 0.f); + check_step(ecn, 2, "Developer", 210, 5.5f, 5.f, 40.8f, 41.f, 41.2f); + check_step(ecn, 3, "Stop Bath", 60, 3.5f, 3.f, 27.f, 38.f, 38.f); + check_step(ecn, 4, "Wash", 180, 3.5f, 3.f, 27.f, 38.f, 38.f); + check_step(ecn, 5, "Bleach", 150, 5.5f, 5.f, 27.f, 38.f, 38.f); + check_step(ecn, 6, "Fixer", 120, 5.5f, 5.f, 27.f, 38.f, 38.f); + check_step(ecn, 7, "Wash 2", 300, 3.5f, 3.f, 27.f, 38.f, 38.f); + check_step(ecn, 8, "Fin Rinse", 120, 1.f, 1.f, 27.f, 38.f, 38.f); + + expect_str(bw->name, "B&W", "B&W name"); + expect_u8(bw->step_count, 7, "B&W count"); + check_step(bw, 0, "Developer", 510, 5.5f, 5.f, 19.f, 20.f, 21.f); + check_step(bw, 1, "Stop", 30, 3.5f, 3.f, 19.f, 20.f, 21.f); + check_step(bw, 2, "Fix", 300, 3.5f, 3.f, 19.f, 20.f, 21.f); + check_step(bw, 3, "Rinse 1", 60, 3.5f, 3.f, 19.f, 20.f, 21.f); + check_step(bw, 4, "Rinse 2", 90, 3.5f, 3.f, 19.f, 20.f, 21.f); + check_step(bw, 5, "Rinse 3", 120, 3.5f, 3.f, 19.f, 20.f, 21.f); + check_step(bw, 6, "Fin Rinse", 30, 1.f, 1.f, 19.f, 20.f, 21.f); + + expect_str(custom->name, "Custom", "Custom name"); + expect_u8(custom->step_count, 4, "Custom count"); + check_step(custom, 0, "Developer", 10, 5.5f, 5.f, 19.f, 20.f, 21.f); + check_step(custom, 1, "Stop", 10, 3.5f, 3.f, 19.f, 20.f, 21.f); + check_step(custom, 2, "Fix", 10, 3.5f, 3.f, 19.f, 20.f, 21.f); + check_step(custom, 3, "Rinse", 10, 3.5f, 3.f, 19.f, 20.f, 21.f); + + expect_str(bwrev->name, "B&WREV", "B&WREV name"); + expect_u8(bwrev->step_count, 12, "B&WREV count"); + check_step(bwrev, 0, "FirstDev", 720, 5.5f, 5.f, 19.5f, 20.f, 20.5f); + check_step(bwrev, 1, "Wash 1", 300, 3.5f, 3.f, 15.5f, 20.f, 22.5f); + check_step(bwrev, 2, "Bleach", 300, 5.5f, 5.f, 19.5f, 20.f, 22.5f); + check_step(bwrev, 3, "Wash 2", 60, 3.5f, 3.f, 15.5f, 20.f, 22.5f); + check_step(bwrev, 4, "Clearing", 120, 5.5f, 5.f, 19.5f, 20.f, 22.5f); + check_step(bwrev, 5, "Wash 3", 60, 3.5f, 3.f, 15.5f, 20.f, 22.5f); + check_step(bwrev, 6, "Reversal", 120, 5.5f, 5.f, 19.5f, 20.f, 22.5f); + check_step(bwrev, 7, "SecondDev", 360, 5.5f, 5.f, 19.5f, 20.f, 22.5f); + check_step(bwrev, 8, "Wash 4", 60, 3.5f, 3.f, 15.5f, 20.f, 22.5f); + check_step(bwrev, 9, "Fix", 300, 5.5f, 5.f, 19.5f, 20.f, 22.5f); + check_step(bwrev, 10, "Wash 5", 60, 3.5f, 3.f, 15.5f, 20.f, 22.5f); + check_step(bwrev, 11, "Fin Rinse", 60, 3.5f, 3.f, 15.5f, 20.f, 22.5f); + + /* Fix broken overlay default check: compare via id, not pointer arithmetic */ + expect_u16(app_process_time_s(PROC_C41, 0), 180, "C41[0] time default"); + expect_u16(app_process_time_s(PROC_C41, 1), 195, "C41[1] time default"); + + static const uint8_t ids[PROCESS_COUNT] = { + PROC_C41, PROC_E6, PROC_ECN2, PROC_BW, PROC_CUSTOM, PROC_BWREV + }; + for (int i = 0; i < PROCESS_COUNT; i++) { + const process_def_t *p = app_process_get(ids[i]); + const process_def_t *by_name = app_process_find_name(p->name); + if (by_name != p) { + fprintf(stderr, "FAIL: find_name(%s) != get(%u)\n", p->name, ids[i]); + g_failures++; + } + } + + expect_err(app_process_adjust_time(PROC_C41, 1, 5), ESP_OK, "adjust C41 step1 +5"); + expect_u16(app_process_time_s(PROC_C41, 1), 200, "C41[1] after +5"); + expect_u16(c41->steps[1].time_s, 195, "const C41[1].time_s unchanged"); + app_process_reset_session(PROC_C41); + expect_u16(app_process_time_s(PROC_C41, 1), 195, "C41[1] after reset"); + + expect_err(app_process_adjust_time(6, 0, 1), ESP_ERR_INVALID_ARG, "bad id"); + expect_err(app_process_adjust_time(PROC_C41, 7, 1), ESP_ERR_INVALID_ARG, "bad step"); + + if (g_failures == 0) { + printf("test_process: all assertions passed\n"); + return 0; + } + fprintf(stderr, "test_process: %d failure(s)\n", g_failures); + return 1; +}