Compare commits
15
Commits
main
...
CI-node-patch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b5c7a096f | ||
|
|
8f5b41ebba | ||
|
|
c047d5d96f | ||
|
|
461771cc02 | ||
|
|
9b2c6bf3ee | ||
|
|
415c13a605 | ||
|
|
d8397778d5 | ||
|
|
255db87881 | ||
|
|
179efb54cd | ||
|
|
01cc76b8b1 | ||
|
|
a9298ae521 | ||
|
|
2458a61625 | ||
|
|
d8fed43b42 | ||
|
|
d6a33996ce | ||
|
|
9558e58e43 |
@@ -0,0 +1,81 @@
|
||||
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: Install host toolchain
|
||||
if: hashFiles('tests/host/CMakeLists.txt') != ''
|
||||
run: |
|
||||
export DEBIAN_FRONTEND=noninteractive && \
|
||||
if command -v sudo >/dev/null 2>&1 && [ "$(id -u)" -ne 0 ]; then SUDO=sudo; else SUDO=; fi && \
|
||||
$SUDO apt-get update && \
|
||||
$SUDO apt-get install -y --no-install-recommends cmake gcc g++ make
|
||||
- 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:
|
||||
- name: Install Node.js
|
||||
run: |
|
||||
export DEBIAN_FRONTEND=noninteractive && \
|
||||
if command -v sudo >/dev/null 2>&1 && [ "$(id -u)" -ne 0 ]; then SUDO=sudo; else SUDO=; fi && \
|
||||
$SUDO apt-get update && \
|
||||
$SUDO apt-get install -y --no-install-recommends curl ca-certificates && \
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | $SUDO bash - && \
|
||||
$SUDO apt-get install -y --no-install-recommends nodejs
|
||||
- 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
|
||||
|
||||
@@ -3,3 +3,8 @@
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
.vscode/ipch
|
||||
build/
|
||||
sdkconfig
|
||||
sdkconfig.old
|
||||
managed_components/
|
||||
dependencies.lock
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# Agent conventions — AutoFilm-ESP32
|
||||
|
||||
Instructions for humans and coding agents working in this repository.
|
||||
|
||||
## Product constraints
|
||||
|
||||
- Default temperatures, step times, and CW/CCW rotation counts for C41, E6, ECN-2, B&W, Custom, and B&WREV **do not change** unless a commit message explicitly says so and the recipe table in `docs/CURRENT_STATE.md` is updated in the same change.
|
||||
- The machine **must always be stoppable** while a step is running: keypad Esc (`'X'`, 4 across / 4 down) and, on the S3 panel, a STOP control. Stop drops motor enable immediately, then offers Resume or return to step select.
|
||||
- UI must not block on motor motion, OneWire conversion, or melody playback.
|
||||
|
||||
## Branch and docs
|
||||
|
||||
- Integration branch is **`develop`**. Feature and refactor work is done on a branch cut from `develop`, then opened as a PR **into `develop`**.
|
||||
- Do not PR this work into `main`. `main` is not the day-to-day integration line.
|
||||
- Active work branch for this refactor: `refactor/esp-idf-modular-ui` (rebase onto `develop` if the branch still points at `main`).
|
||||
- Architecture intent: `docs/CURRENT_STATE.md`, `docs/TARGET_ARCHITECTURE.md`, `docs/CICD.md`.
|
||||
- Implementation sessions: `docs/megaplans/REFACTOR-MEGAPLAN.md` then exactly one `docs/megaplans/refactor/Pxx-*.md`. Do not start the next phase in the same session.
|
||||
- Do not commit secrets (Git tokens, Wi-Fi passwords, `sdkconfig` with provisioned keys).
|
||||
|
||||
## Layout
|
||||
|
||||
| Path | Role |
|
||||
| --- | --- |
|
||||
| `src/`, `include/` | Legacy Arduino sources; shrink, do not grow, except bugfixes needed for the port |
|
||||
| `main/` | IDF entry |
|
||||
| `components/app_*` | Application: process table, machine, UI |
|
||||
| `components/ui_cmd` | Command/event types only |
|
||||
| `components/hal_*` | Hardware adapters |
|
||||
| `components/board_*` | Pin maps and board bring-up |
|
||||
| `docs/` | Design notes |
|
||||
| `.gitea/workflows/` | Gitea Actions (tests + firmware builds) |
|
||||
|
||||
New behaviour goes into `components/`, not into larger `menu.cpp` files.
|
||||
|
||||
## Framework policy
|
||||
|
||||
- Build system is **ESP-IDF**. PlatformIO + Arduino remains only until the IDF skeleton compiles both boards.
|
||||
- **Arduino-as-component** is allowed as a bridge for AccelStepper, Keypad, LiquidCrystal_I2C, OneWire, DallasTemperature.
|
||||
- Treat Arduino as debt:
|
||||
- No Arduino types (`String`, `Keypad`, `AccelStepper`, `LiquidCrystal_I2C`) in headers of `app_machine`, `app_process`, or `ui_cmd`.
|
||||
- Do not add new Arduino library dependencies.
|
||||
- Prefer IDF drivers (GPIO, RMT, I2C, LEDC, `esp_timer`, `esp_lcd`) for new work.
|
||||
- Two firmware flavours: `board_wroom` and `board_jc4827w543`. Gate pins and peripherals with board CMake, not `#ifdef` sprinkled through the state machine.
|
||||
|
||||
## Concurrency
|
||||
|
||||
- One responsibility per task: input, UI, machine, motor, temperature, audio.
|
||||
- Cross-task data: FreeRTOS queues, task notifications, atomics. No unsynchronised use of `lcd` / `stepper` / `sensors`.
|
||||
- ISR / stop path: set a flag, disable motor EN, `xQueueSendFromISR` / `xTaskNotifyFromISR`. No heap, no `delay`, no LCD from ISR.
|
||||
- Do not `vTaskDelete` worker tasks after startup. Do not disable the task watchdog to “make it work”.
|
||||
- `vTaskDelay` / `esp_timer` instead of busy-wait `millis()` loops.
|
||||
|
||||
## UI / machine split
|
||||
|
||||
- UI emits commands; machine emits events. See `docs/TARGET_ARCHITECTURE.md`.
|
||||
- Touch and keypad are input adapters. They produce the same commands.
|
||||
- Display adapters render events. Machine code must compile with no display driver linked (except in tests that stub HAL).
|
||||
|
||||
## Code style
|
||||
|
||||
- C11 for new IDF components; C++17 only where an existing Arduino library forces it, isolated in `hal_*`.
|
||||
- `snake_case` for new functions and files; existing camelCase stays until that file is rewritten.
|
||||
- 4-space indent to match the current tree.
|
||||
- No raw `new` / `malloc` for per-frame strings. No `String` concatenation on the control path.
|
||||
- Log with `ESP_LOGx` tags (`machine`, `motor`, `ui`, `temp`, `input`), not `Serial.print`.
|
||||
- Errors are `esp_err_t` across HAL boundaries.
|
||||
|
||||
## Recipes
|
||||
|
||||
- Recipe tables live in `app_process` (or today’s `devSequence.cpp` until moved).
|
||||
- Session time nudges (±5 s) are RAM overlays, not edits of the const defaults.
|
||||
- Persistent custom profiles are out of scope until designed; do not half-add NVS.
|
||||
|
||||
## Hardware notes
|
||||
|
||||
- WROOM pins are defined in the legacy `config` headers; do not silently reuse them on the S3 panel.
|
||||
- JC4827W543C: NV3041A 480×272 QSPI, GT911 on I²C, limited free IO. Confirm any new GPIO against the board pin table before assigning STEP/DIR/EN/1-Wire.
|
||||
- Motor enable is active LOW on the current WROOM wiring. Safe state is disabled.
|
||||
|
||||
## Commits and review
|
||||
|
||||
- Cut work from `develop`. Open PRs against `develop`.
|
||||
- Imperative commit subjects (`Add machine stop notify`, not `Added`).
|
||||
- One concern per commit when practical (HAL wrap ≠ UI rewrite ≠ recipe move).
|
||||
- Do not reformat whole legacy files in the same commit as a behaviour change.
|
||||
- If defaults would change, fail the change unless the user explicitly asked.
|
||||
|
||||
## Tests
|
||||
|
||||
- Prefer host-side tests for `app_process` lookup and the machine state machine with a stub motor (`tests/host`, CMake + CTest).
|
||||
- On-target smoke: select process, start a short Custom step, Esc mid-step, confirm EN high and Resume works.
|
||||
- CI is Gitea Actions: host tests must pass before either firmware image is built. See `docs/CICD.md`.
|
||||
- After `tests/host` exists, run it before push. After the IDF skeleton exists, a local `idf.py` build for the board you touched is expected.
|
||||
@@ -0,0 +1,3 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(autofilm)
|
||||
@@ -0,0 +1,3 @@
|
||||
idf_component_register(SRCS "app_machine.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES ui_cmd app_process)
|
||||
@@ -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 <string.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#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);
|
||||
@@ -0,0 +1,3 @@
|
||||
idf_component_register(SRCS "app_process.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES ui_cmd)
|
||||
@@ -0,0 +1,148 @@
|
||||
#include "app_process.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
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]));
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#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);
|
||||
@@ -0,0 +1 @@
|
||||
idf_component_register(SRCS "board.c" INCLUDE_DIRS "include" REQUIRES driver)
|
||||
@@ -0,0 +1,16 @@
|
||||
#include "board.h"
|
||||
|
||||
const char *board_name(void)
|
||||
{
|
||||
return "jc4827w543";
|
||||
}
|
||||
|
||||
gpio_num_t board_pin_motor_en(void)
|
||||
{
|
||||
return GPIO_NUM_NC;
|
||||
}
|
||||
|
||||
int board_motor_en_disable_level(void)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
#include "driver/gpio.h"
|
||||
const char *board_name(void);
|
||||
gpio_num_t board_pin_motor_en(void);
|
||||
int board_motor_en_disable_level(void);
|
||||
@@ -0,0 +1 @@
|
||||
idf_component_register(SRCS "board.c" INCLUDE_DIRS "include" REQUIRES driver)
|
||||
@@ -0,0 +1,16 @@
|
||||
#include "board.h"
|
||||
|
||||
const char *board_name(void)
|
||||
{
|
||||
return "wroom";
|
||||
}
|
||||
|
||||
gpio_num_t board_pin_motor_en(void)
|
||||
{
|
||||
return (gpio_num_t)27;
|
||||
}
|
||||
|
||||
int board_motor_en_disable_level(void)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
#include "driver/gpio.h"
|
||||
const char *board_name(void);
|
||||
gpio_num_t board_pin_motor_en(void);
|
||||
int board_motor_en_disable_level(void);
|
||||
@@ -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);
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#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);
|
||||
@@ -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 */
|
||||
@@ -0,0 +1 @@
|
||||
idf_component_register(INCLUDE_DIRS "include")
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#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;
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
# CI/CD — Gitea Actions
|
||||
|
||||
Status: planned on `refactor/esp-idf-modular-ui`. Implementation follows the IDF skeleton
|
||||
(migration step 2 in `docs/TARGET_ARCHITECTURE.md`). Workflows live in **`.gitea/workflows/`**
|
||||
(Gitea Actions, GitHub-compatible YAML). PRs target **`develop`**.
|
||||
|
||||
## Purpose
|
||||
|
||||
Every change that lands on `develop` must:
|
||||
|
||||
1. Run **host unit tests** for recipe tables and the machine state machine (no hardware).
|
||||
2. **Build two firmware images**: `board_wroom` (ESP32 classic) and `board_jc4827w543` (ESP32-S3).
|
||||
3. Publish flashable artifacts (app binary, bootloader, partition table, merged image, size report).
|
||||
|
||||
Legacy PlatformIO (`platformio.ini`) is not a CI target.
|
||||
|
||||
## Runner requirements
|
||||
|
||||
Gitea `act_runner` must be registered against `git.i3omb.com` with labels that match `runs-on`.
|
||||
|
||||
| Workflow `runs-on` | Typical runner label | Notes |
|
||||
| --- | --- | --- |
|
||||
| `ubuntu-latest` | `ubuntu-latest:docker://gitea/runner-images:ubuntu-latest` | Host tests, checkout, artifact upload |
|
||||
| (job `container`) | Docker socket available on the runner | Firmware jobs use `espressif/idf:<tag>` as the job container |
|
||||
|
||||
Firmware builds need:
|
||||
|
||||
- Docker on the runner (job-level `container:`)
|
||||
- Enough disk for the IDF image (~several GB) and two `build/` trees
|
||||
- Outbound pull of `docker.io/espressif/idf` (or a mirror on the Gitea registry)
|
||||
|
||||
If the instance cannot pull Docker Hub, mirror `espressif/idf:v5.4` into the Gitea container registry and point `container.image` at that.
|
||||
|
||||
Do not depend on `espressif/esp-idf-ci-action` or other GitHub-hosted actions for the compile step. Those often assume GitHub path layout. Drive `idf.py` inside `espressif/idf` instead. `actions/checkout` and `actions/upload-artifact` (or Gitea’s equivalents) are fine.
|
||||
|
||||
## Triggers
|
||||
|
||||
| Event | What runs |
|
||||
| --- | --- |
|
||||
| `pull_request` into `develop` | tests + both firmware builds (required check) |
|
||||
| `push` to `develop` | tests + both firmware builds; keep artifacts |
|
||||
| `push` to `refactor/**` and `feature/**` | same, so the branch is green before the PR |
|
||||
| `push` tags `v*` | tests + builds + attach binaries to the Gitea release |
|
||||
| `workflow_dispatch` | manual rebuild |
|
||||
|
||||
`main` is not a CI integration branch for this refactor.
|
||||
|
||||
## Jobs
|
||||
|
||||
```
|
||||
test (host) ──► firmware [wroom / jc4827w543] ──► (tag only) release
|
||||
```
|
||||
|
||||
Firmware must `needs: test`. A failing recipe or state-machine test must not produce a binary.
|
||||
|
||||
### 1. `test` — host unit tests
|
||||
|
||||
Runs on `ubuntu-latest` **without** the IDF container so the gate is fast.
|
||||
|
||||
- Toolchain: CMake + GCC (C11), CTest.
|
||||
- Tree: `tests/host/` linking `components/app_process` and `components/app_machine` against **stub HAL** (`tests/host/stubs/`: motor, temp, audio, display no-ops; queues replaced with a tiny fake or a vendored FreeRTOS POSIX port if the machine task is exercised).
|
||||
- Framework: Unity (can be vendored) or plain `assert` + CTest. Prefer Unity so on-target tests later share the same style.
|
||||
- Must cover:
|
||||
- `find` / lookup of all six process names
|
||||
- frozen default times, temps, CW/CCW for every step (values from `docs/CURRENT_STATE.md`)
|
||||
- session ±5 s overlay does not mutate the const table
|
||||
- state machine: Idle → Select → Arm → Start → Stop (EN disabled) → Resume → Complete → Return
|
||||
- `Stop` is accepted in `StepRunning` and during a complete-alarm
|
||||
- Recipe-value tests fail the build if defaults change without an explicit doc update.
|
||||
|
||||
Command sketch:
|
||||
|
||||
```bash
|
||||
cmake -S tests/host -B build/host
|
||||
cmake --build build/host
|
||||
ctest --test-dir build/host --output-on-failure
|
||||
```
|
||||
|
||||
Until `tests/host` exists, the workflow file is not merged as a required check — or the job is `if`’d on path existence. Do not ship a red pipeline on docs-only commits after the workflow is enabled; use:
|
||||
|
||||
```yaml
|
||||
- name: Host tests
|
||||
if: hashFiles('tests/host/CMakeLists.txt') != ''
|
||||
```
|
||||
|
||||
### 2. `firmware` — matrix build
|
||||
|
||||
Job container: `espressif/idf:v5.4` (pin the tag; bump in one place when IDF is upgraded).
|
||||
|
||||
Matrix:
|
||||
|
||||
| Name | `IDF_TARGET` | Board cmake/sdkconfig |
|
||||
| --- | --- | --- |
|
||||
| `wroom` | `esp32` | `-DAUTOFILM_BOARD=wroom` + `sdkconfig.defaults` + `sdkconfig.wroom` |
|
||||
| `jc4827w543` | `esp32s3` | `-DAUTOFILM_BOARD=jc4827w543` + `sdkconfig.defaults` + `sdkconfig.s3` |
|
||||
|
||||
Steps:
|
||||
|
||||
1. Checkout (full history not required).
|
||||
2. `source /opt/esp/idf/export.sh` (already true in the official image entrypoint; use `idf.py` directly).
|
||||
3. `idf.py set-target ${IDF_TARGET}` then
|
||||
`idf.py @sdkconfig.defaults @sdkconfig.<board> -DAUTOFILM_BOARD=<board> build`
|
||||
4. `idf.py size` (fail the job only if the binary will not fit flash; log the report either way).
|
||||
5. Merge a single flash image:
|
||||
`esptool.py --chip ${IDF_TARGET} merge_bin -o autofilm-${board}-${GIT_SHA}.bin @build/flash_args`
|
||||
6. Upload artifacts (see below).
|
||||
|
||||
S3 build must enable octal PSRAM in `sdkconfig.s3`. WROOM must not.
|
||||
|
||||
Arduino-as-component is pulled by the IDF CMake on the WROOM image first; the S3 image may share it until debt burn-down. Cache `managed_components/` by lock hash when a `idf_component.yml` exists.
|
||||
|
||||
### 3. `release` (tags only)
|
||||
|
||||
`needs: [test, firmware]`. Download both artifact sets and attach to the Gitea release for tag `v*`:
|
||||
|
||||
- `autofilm-wroom-<version>.bin` (merged)
|
||||
- `autofilm-jc4827w543-<version>.bin` (merged)
|
||||
- raw `app.bin` / `bootloader.bin` / `partition-table.bin` per board
|
||||
- `sdkconfig` snapshot per board
|
||||
- `size.txt` per board
|
||||
|
||||
Version string is the git tag. CI also stamps `version` in firmware via `-DAUTOFILM_GIT_DESC=$(git describe --always --dirty)`.
|
||||
|
||||
## Artifacts
|
||||
|
||||
| File | Why |
|
||||
| --- | --- |
|
||||
| `autofilm-<board>-<sha>.bin` | Merged image for `esptool write_flash 0x0` |
|
||||
| `build/autofilm.bin` (app) | OTA / partial flash |
|
||||
| `build/bootloader/bootloader.bin` | First-time flash |
|
||||
| `build/partition_table/partition-table.bin` | First-time flash |
|
||||
| `build/flasher_args.json` | Exact offsets |
|
||||
| `size.txt` / `size.json` | Flash/RAM budget |
|
||||
| `sdkconfig` | Repro the build flags |
|
||||
|
||||
Retention: 14 days on branch builds, keep release attachments. Artifact names must include board + short SHA so WROOM and S3 cannot overwrite each other.
|
||||
|
||||
## Example workflow (target shape)
|
||||
|
||||
Path: `.gitea/workflows/ci.yml`
|
||||
|
||||
```yaml
|
||||
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
|
||||
```
|
||||
|
||||
Context substitutions: Gitea accepts both `github.*` and `gitea.*` in many versions; prefer `gitea.sha` / `gitea.ref` and fall back if the runner is older.
|
||||
|
||||
`hashFiles` guards keep docs-only stages of this branch from going red before the IDF tree exists.
|
||||
|
||||
## Required status on PRs
|
||||
|
||||
Once the IDF skeleton compiles, mark on the `develop` branch protection (Gitea repo settings):
|
||||
|
||||
- `test`
|
||||
- `firmware (wroom)`
|
||||
- `firmware (jc4827w543)`
|
||||
|
||||
No merge to `develop` with a red firmware matrix.
|
||||
|
||||
## Caching (optional, second pass)
|
||||
|
||||
- Cache `~/.cache/pip` and IDF tools if not using the pre-baked `espressif/idf` image.
|
||||
- Cache `managed_components/` keyed on `idf_component.yml` + `idf.lock`.
|
||||
- Do not cache `build/` across boards or targets.
|
||||
|
||||
## Secrets
|
||||
|
||||
CI must not need device tokens or Wi-Fi credentials. Firmware builds are offline-configurable. If a later job publishes to a hardware farm, use Gitea secrets — never commit them.
|
||||
|
||||
## Local equivalent
|
||||
|
||||
```bash
|
||||
cmake -S tests/host -B build/host && cmake --build build/host && ctest --test-dir build/host --output-on-failure
|
||||
|
||||
docker run --rm -v "$PWD":/project -w /project espressif/idf:v5.4 \
|
||||
bash -lc 'idf.py -B build/wroom set-target esp32 && idf.py -B build/wroom -D AUTOFILM_BOARD=wroom build'
|
||||
```
|
||||
|
||||
Agents must run the host tests before pushing once `tests/host` exists.
|
||||
@@ -0,0 +1,168 @@
|
||||
# AutoFilm-ESP32 — Current State
|
||||
|
||||
Snapshot of the pre-refactor PlatformIO Arduino tree (`9186d84`).
|
||||
Integration going forward is **`develop`**; this refactor lands via PR from `refactor/esp-idf-modular-ui` into `develop`.
|
||||
This document describes the tree **as it existed at that snapshot**, before the ESP-IDF modular refactor. Recipe *values* (times, temperatures, rotation counts) are treated as frozen; this document does not propose changing them.
|
||||
|
||||
## What the product is
|
||||
|
||||
AutoFilm-ESP32 is a Kindermann-style rotary film processor controller. The operator picks a process (C41, E6, B&W, ECN-2, Custom, B&WREV), walks the chemical steps, starts a timed agitation cycle, and is prompted to pour / drain between steps. Temperature is *measured* only; closed-loop bath heat is an external sous-vide.
|
||||
|
||||
## Toolchain and layout
|
||||
|
||||
| Item | Today |
|
||||
| --- | --- |
|
||||
| Build | PlatformIO `platformio.ini`, env `esp32dev` |
|
||||
| Framework | Arduino on Espressif32 |
|
||||
| Target | Classic ESP32-WROOM (`board = esp32dev`) |
|
||||
| Layout | `src/*.cpp`, `include/*.h`, Arduino-style `setup()` / `loop()` |
|
||||
| Entry | `src/AutoFilmESP32.cpp` |
|
||||
|
||||
Arduino libraries (all treated as future debt):
|
||||
|
||||
- `AccelStepper` — STEP/DIR stepper
|
||||
- `LiquidCrystal_I2C` — 20×4 character LCD
|
||||
- `Keypad` — matrix scan
|
||||
- `OneWire` + `DallasTemperature` — DS18B20
|
||||
|
||||
There is **no** ESP-IDF `CMakeLists.txt`, no `sdkconfig`, no component split, no CI matrix.
|
||||
|
||||
## Hardware as wired in this tree
|
||||
|
||||
From `include/config.h` and `src/config.cpp`:
|
||||
|
||||
| Function | GPIO / bus |
|
||||
| --- | --- |
|
||||
| Stepper STEP (pulse) | 12 |
|
||||
| Stepper DIR | 14 |
|
||||
| Stepper EN (active LOW) | 27 |
|
||||
| DS18B20 data | 13 |
|
||||
| Beeper | 25 |
|
||||
| LCD I²C | SDA 21, SCL 22, address `0x27`, 20×4 |
|
||||
| Keypad rows | 19, 18, 5, 17, 16 |
|
||||
| Keypad cols | 15, 2, 0, 4 |
|
||||
|
||||
Motor constants: `STEPS_PER_REV = 4800`, `RPM = 60`, acceleration `9600`. Enable is driven HIGH at boot (disabled).
|
||||
|
||||
Keypad map (5×4):
|
||||
|
||||
```
|
||||
F E # *
|
||||
1 2 3 U
|
||||
4 5 6 D
|
||||
7 8 9 X ← 4 across, 4 down = Esc ('X')
|
||||
L 0 R E
|
||||
```
|
||||
|
||||
Keys `1`–`6` select programmes. `U`/`D` browse steps. `L`/`R` nudge step time ±5 s. `E` confirm. `X` escape.
|
||||
|
||||
## Module map
|
||||
|
||||
```
|
||||
AutoFilmESP32.cpp setup/loop, LCD splash, watchdog off
|
||||
config.cpp / .h globals, pin map, library objects
|
||||
devSequence.cpp/.h six compiled-in recipes
|
||||
menu.cpp / .h blocking menus + startProcessing()
|
||||
display.cpp / .h starting menu, headings, mm:ss helper
|
||||
temperature.cpp/.h DS18B20 read + LCD write
|
||||
motor.cpp / .h FreeRTOS agitation task
|
||||
sound.cpp / .h blocking tone() tunes
|
||||
watchdog.h disables TWDT and idle-task WDT
|
||||
```
|
||||
|
||||
There is no HAL. UI, recipes, and actuators share globals (`devPgm`, `run`, `lcd`, `stepper`, `keypad`, task handles).
|
||||
|
||||
## Recipe data
|
||||
|
||||
`struct devSequence` (`include/devSequence.h`):
|
||||
|
||||
- `processName[7]`
|
||||
- `cycles` — number of steps
|
||||
- `processTime[20]` — seconds
|
||||
- `processCycleName[20][10]`
|
||||
- `processCycle[2][20]` — `[0]` CW revolutions, `[1]` CCW revolutions
|
||||
- `processTemp[3][20]` — `[0]` min, `[1]` preferred, `[2]` max °C
|
||||
|
||||
`NUM_DEV_SEQUENCES = 6`. Lookup is linear `strcmp` in `findSequenceByName()`.
|
||||
|
||||
### Compiled defaults (do not change values)
|
||||
|
||||
**C41** — 7 steps, ~38 °C developer:
|
||||
|
||||
| Step | Time s | Name | CW | CCW | Min | Pref | Max |
|
||||
| --- | ---: | --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| 0 | 180 | Prewarm | 1 | 1 | 37.8 | 38 | 38.2 |
|
||||
| 1 | 195 | Developer | 5.5 | 5 | 37.8 | 38 | 38.2 |
|
||||
| 2 | 45 | Bleach | 5.5 | 5 | 32 | 38 | 38.2 |
|
||||
| 3 | 180 | Fix | 5.5 | 5 | 32 | 38 | 38.2 |
|
||||
| 4 | 60 | Rinse 1 | 3.5 | 3 | 32 | 38 | 38.2 |
|
||||
| 5 | 60 | Rinse 2 | 3.5 | 3 | 32 | 38 | 38.2 |
|
||||
| 6 | 30 | Fin Rinse | 1 | 1 | 32 | 38 | 38.2 |
|
||||
|
||||
**E6** — 12 steps (Preheat … Fin Rinse). FirstDev 360 s at 38 °C; ColorDev 360 s; Fin Rinse 30 s at 19–21 °C.
|
||||
|
||||
**ECN-2** — 9 steps. RemJet is a 0 s / 0 rotation placeholder (manual). Developer 210 s at 40.8–41.2 °C.
|
||||
|
||||
**B&W** — 7 steps. Developer 510 s at 19–21 °C.
|
||||
|
||||
**Custom** — 4×10 s stub (Developer / Stop / Fix / Rinse).
|
||||
|
||||
**B&WREV** — 12 steps. FirstDev 720 s; SecondDev 360 s; wash steps cooler (min 15.5 °C).
|
||||
|
||||
Left/right time edits mutate the in-RAM struct. They are **not** written to flash. Process names longer than 6 characters would not fit `processName[7]`.
|
||||
|
||||
## Runtime behaviour
|
||||
|
||||
1. `setup()`: Serial 115200, **watchdogs disabled**, I²C + LCD init, custom thermometer glyph, motor disabled, splash `AUTOFILM` + `V0.1.1 20240701` for 1 s.
|
||||
2. `loop()` calls `startingMenu()` and never returns to a real idle loop while a programme is selected.
|
||||
3. `startingMenu()` draws 1–6 and **busy-waits** until `devPgm != ""`.
|
||||
4. `startDev()` walks `cycles`. If `run == 1` it chains `startProcessing()` for the next step (auto-advance after a completed step). Otherwise it shows Step / Time / Temp and waits for U/D/E/X/L/R.
|
||||
5. `startProcessing()`:
|
||||
- Shows step name, duration, preferred temp, `Ent:start Esc:quit`.
|
||||
- Esc (`X`) here aborts **before** motion and returns 0.
|
||||
- On Enter, records `processStartTime` / `processTimeMillis`, heap-allocates `MotorTaskParams`, and `xTaskCreatePinnedToCore(runMotorTask, …, core 0)`.
|
||||
- Then **blocks** in `while (millis() < end) { update remaining; delay(410); readTemperature(); }`.
|
||||
- Deletes the motor task, drives EN HIGH, plays a **blocking 10-beep alarm** (~7.5 s), frees params.
|
||||
|
||||
`runMotorTask` enables the driver and loops forever: CW `cwRotations` revs, then CCW `ccwRotations` revs, using `stepper.run()` busy-wait on each move. `processEndTime` is passed in and **never read**. The task does not check a stop flag. It only ends when the UI task `vTaskDelete`s it.
|
||||
|
||||
Temperature: `sensors.requestTemperatures()` + `getTempCByIndex(0) + 0.4 °C` offset, written to LCD row 3. Called from the UI wait loops. `updateTempDisplay(void *)` exists as a one-shot task that also talks to the LCD and then deletes itself; it is unused from `startProcessing()`.
|
||||
|
||||
Sound: `tone()` + `delay()` on the caller thread. `playTune()` is unused by the main flow.
|
||||
|
||||
## Concurrency as it stands
|
||||
|
||||
| Thread | Role | Problem |
|
||||
| --- | --- | --- |
|
||||
| Arduino `loopTask` | All UI, all input, all timing, temp, sound | Blocks for seconds to minutes |
|
||||
| `MotorTask` (core 0) | Agitation | No cooperative stop; killed from outside |
|
||||
| Idle 0/1 | — | Removed from TWDT |
|
||||
|
||||
LCD, `stepper`, and `sensors` are used without mutexes. That is safe only because the UI thread does not command the stepper while the motor task runs — except `vTaskDelete` can interrupt `stepper.run()` mid-pulse.
|
||||
|
||||
## Defects and hazards relevant to the refactor
|
||||
|
||||
1. **No stop while a step is running.** Esc is honoured only on the pre-start prompt. The run loop does not read the keypad. This violates the product rule that the machine must always stop on Esc.
|
||||
2. **`vTaskDelete` of a live motor task** can leave STEP/DIR in an undefined state; EN is dropped only *after* delete.
|
||||
3. **Watchdog deliberately off** (`include/watchdog.h`). A hung `stepper.run()` or OneWire slot will wedge the chip with no recovery.
|
||||
4. **Busy-wait input** (`while (true)` + `keypad.getKey()`). `millis() % 1000 == 0` is not a 1 Hz tick; it is a one-millisecond race and can skip or repeat temp reads.
|
||||
5. **`secondsToMinutesSeconds()` `malloc`s 6 bytes and never frees.** Called from the run loop ~twice per second.
|
||||
6. **`startDev()` shadows the global** with `int run = 1;` on Enter, so the auto-advance path is inconsistent with the `run` flag used after `startProcessing()` returns.
|
||||
7. **OneWire conversion blocks the UI** (typically 750 ms at 12-bit). Combined with `delay(410)` the remaining-time display stutters.
|
||||
8. **Alarm after a step blocks everything** for several seconds; Esc cannot interrupt it.
|
||||
9. **No input abstraction.** Touch cannot be added without rewriting `menu.cpp`.
|
||||
10. **Board pins are compile-time literals.** The Guition S3 panel reuses many of these GPIOs for QSPI / GT911; the current map cannot be copied across.
|
||||
|
||||
## What is *not* in the tree
|
||||
|
||||
- ESP-IDF project or dual-firmware CI
|
||||
- Touch / colour LCD support
|
||||
- Pause / resume of a live step
|
||||
- Persistent profiles (README already lists this as future)
|
||||
- Pumps, valves, reservoirs, drain actuators
|
||||
- Closed-loop heater control
|
||||
- Unit tests beyond PlatformIO’s empty `test/README`
|
||||
|
||||
## README vs code
|
||||
|
||||
`README.MD` matches the hardware story (ESP32, NEMA-17, DS18B20, 2004 I²C, Kindermann tank) and the pour-start-drain loop. It still talks about opening the project in Arduino IDE / PlatformIO and does not mention FreeRTOS, the S3 panel, or stop-during-run.
|
||||
@@ -0,0 +1,308 @@
|
||||
# AutoFilm-ESP32 — Target Architecture
|
||||
|
||||
Status: proposed on `refactor/esp-idf-modular-ui`, for review via PR into **`develop`**.
|
||||
Does not change default process times, temperatures, or rotation patterns.
|
||||
|
||||
## Goals
|
||||
|
||||
1. Rebuild as an **ESP-IDF** project that still links **Arduino-as-component** so AccelStepper, Keypad, LiquidCrystal_I2C, OneWire, and DallasTemperature keep working in the first cut.
|
||||
2. Produce **two firmware images in CI**: classic ESP32-WROOM (2004 + keypad) and Guition **JC4827W543C** (ESP32-S3-WROOM-1-N4R8, NV3041A 480×272 QSPI, GT911 capacitive touch).
|
||||
3. Split **UI / input** from **mechatronics** so a touch panel can replace the character LCD without rewriting agitation or recipes.
|
||||
4. Use FreeRTOS tasks, queues, and interrupts so the UI never blocks on motor moves, OneWire conversions, or tones.
|
||||
5. **Always stop motion on Esc** (keypad position 4 across / 4 down, key `'X'`) and, later, on a touch STOP control. Same command path for both.
|
||||
6. Keep Arduino libraries isolated behind HAL files; replacing them with native IDF drivers is planned debt, not this document’s implementation phase.
|
||||
7. Leave room for pumps, valves, reservoirs, and drain without breaking the command/event contract.
|
||||
|
||||
Arduino-as-component is **explicit technical debt**. New modules must not add Arduino types to public headers.
|
||||
|
||||
## Non-goals (this refactor)
|
||||
|
||||
- Changing C41 / E6 / ECN-2 / B&W / Custom / B&WREV default numbers.
|
||||
- A real Custom-programme editor (session ±5 s only; NVS profiles later).
|
||||
- Closed-loop bath heating.
|
||||
- Implementing pumps/valves now — only the extension points.
|
||||
|
||||
## Dual board model
|
||||
|
||||
| | `board_wroom` | `board_jc4827w543` |
|
||||
| --- | --- | --- |
|
||||
| SoC | ESP32-WROOM (classic) | ESP32-S3-WROOM-1-N4R8 (4 MB flash, 8 MB octal PSRAM) |
|
||||
| UI | 20×4 I²C LCD `0x27` + 5×4 keypad | 4.3" IPS 480×272 NV3041A QSPI + GT911 I²C |
|
||||
| Touch | none | GT911, typical pins SDA 8, SCL 4, INT 3, RST 38, addr `0x5D` |
|
||||
| Display bus | I²C 21/22 | QSPI CS 45, SCK 47, D0–D3 21/48/40/39, backlight GPIO 1 |
|
||||
| Mechatronics | current pin map | **remapped**; panel consumes most GPIOs, ~10 usable extras + headers |
|
||||
|
||||
Pin maps live only in board components. Application code sees `hal_motor_enable()`, not `GPIO_NUM_27`.
|
||||
|
||||
CI builds:
|
||||
|
||||
```
|
||||
idf.py -D AUTOFILM_BOARD=wroom @sdkconfig.defaults @sdkconfig.wroom
|
||||
idf.py -D AUTOFILM_BOARD=jc4827w543 @sdkconfig.defaults @sdkconfig.s3
|
||||
```
|
||||
|
||||
Automated by Gitea Actions — see `docs/CICD.md`. Both images are required checks on PRs into `develop`.
|
||||
|
||||
WROOM remains the machine that already exists. S3 firmware is the future operator panel; mechatronics may stay on the WROOM or move when the S3 I/O map is proven. The **same** `app_machine` binary interface is compiled for both.
|
||||
|
||||
Vendor references for the S3 panel (not copied into this repo): [lsdlsd88/JC4827W543](https://github.com/lsdlsd88/JC4827W543), [profi-max board notes](https://github.com/profi-max/JC4827W543_4.3inch_ESP32S3_board) — NV3041A + GT911, 480×272.
|
||||
|
||||
## Proposed tree
|
||||
|
||||
Existing `src/` and `include/` stay until each file is moved. New code lands here:
|
||||
|
||||
```
|
||||
AGENTS.md
|
||||
docs/
|
||||
CURRENT_STATE.md
|
||||
TARGET_ARCHITECTURE.md
|
||||
main/ # idf app: NVS, task spawn, board select
|
||||
components/
|
||||
app_process/ # recipe table + lookup + session time overrides
|
||||
app_machine/ # process state machine (no display types)
|
||||
app_ui/ # screens + command emission (no stepper types)
|
||||
ui_cmd/ # command / event structs only
|
||||
hal_motor/
|
||||
hal_temp/
|
||||
hal_input/ # keypad + (later) GT911 → ui_event
|
||||
hal_display/ # 2004 adapter; later NV3041A adapter
|
||||
hal_audio/
|
||||
board_wroom/
|
||||
board_jc4827w543/
|
||||
managed_components/ # Arduino-esp32 component, IDF drivers
|
||||
```
|
||||
|
||||
Public headers of `app_*` and `ui_cmd` are C (or `extern "C"`) and Arduino-free.
|
||||
|
||||
## Command / event contract
|
||||
|
||||
UI never starts the motor. UI posts commands. Machine never writes pixels. Machine posts events.
|
||||
|
||||
### Commands (`ui → machine`)
|
||||
|
||||
| Command | Now | Later |
|
||||
| --- | --- | --- |
|
||||
| `SelectProcess(id)` | keypad 1–6 | touch tile |
|
||||
| `BrowseStep(delta)` | U/D | swipe / list |
|
||||
| `AdjustStepTime(step, delta_s)` | L/R ±5 | stepper buttons |
|
||||
| `ArmStep(step)` | show Ent/Esc prompt | confirm sheet |
|
||||
| `StartStep(step)` | Enter | START |
|
||||
| `Stop` | Esc during run | STOP |
|
||||
| `Resume` | after Stop | RESUME |
|
||||
| `ReturnToStepSelect` | after Stop | BACK |
|
||||
| `CancelArmed` | Esc before start | cancel |
|
||||
|
||||
Reserved, not implemented:
|
||||
|
||||
`PumpSet`, `ValveSet`, `ReservoirSelect`, `DrainStart`, `DrainStop`, `HeaterSetpoint`.
|
||||
|
||||
Adding an actuator is a new command + HAL + machine state. It must not require `app_ui` to include motor headers.
|
||||
|
||||
### Events (`machine → ui`)
|
||||
|
||||
| Event | Payload |
|
||||
| --- | --- |
|
||||
| `ProcessSelected` | process id, name, step count |
|
||||
| `StepView` | index, name, time_s, temp pref/min/max, cw, ccw |
|
||||
| `StepArmed` | same |
|
||||
| `StepStarted` | start tick, duration_ms |
|
||||
| `StepProgress` | remaining_ms, temp_c, motion {cw\|ccw\|idle} |
|
||||
| `StepComplete` | index |
|
||||
| `StepStopped` | remaining_ms, index |
|
||||
| `StepResumed` | remaining_ms |
|
||||
| `ProcessIdle` | — |
|
||||
| `TempUpdated` | °C or disconnected |
|
||||
| `Fault` | code, message |
|
||||
|
||||
Required on-screen fields while running: **temperature, step name, time remaining**. Motion direction is optional. STOP / RESUME / BACK are controls, not telemetry.
|
||||
|
||||
### Transport
|
||||
|
||||
One FreeRTOS queue each way (or a tiny pub/sub wrapping `xQueueSend`). Bounded structs, no `String`. ISR context may only `xQueueSendFromISR` a `Stop` or raw key/touch sample.
|
||||
|
||||
```
|
||||
┌────────────┐ cmd_q ┌──────────────┐ actuator calls ┌──────────┐
|
||||
│ app_ui │ ─────────► │ app_machine │ ─────────────────► │ hal_* │
|
||||
│ + hal_in │ ◄───────── │ + app_process│ ◄── temp / done │ │
|
||||
└────────────┘ evt_q └──────────────┘ └──────────┘
|
||||
```
|
||||
|
||||
## State machine
|
||||
|
||||
```
|
||||
Idle → ProcessSelected → StepSelect ⇄ StepArmed
|
||||
│
|
||||
▼
|
||||
StepRunning ◄── Resume
|
||||
│ Stop
|
||||
▼
|
||||
StepStopped → StepSelect
|
||||
│ timer done
|
||||
▼
|
||||
StepComplete → StepSelect (or auto-advance policy later)
|
||||
```
|
||||
|
||||
`run == 1` auto-chaining from the current `startDev()` is preserved as a **machine policy flag** (`auto_advance`), not as UI blocking. Default: after `StepComplete`, arm the next step and wait for Start (safer with pour/drain). If we must match today’s “immediately start the next step after the alarm”, that is one flag, not a second code path.
|
||||
|
||||
### Stop / resume (product rule)
|
||||
|
||||
- **Stop** = immediately end the agitation routine, **disable the motor driver**, keep the step and remaining time, show Resume or Return to step select.
|
||||
- **Resume** = continue the **same step** for the remaining time with the same CW/CCW pattern. Short beep.
|
||||
- **Return** = leave the step stopped, motor disabled, back to step list. Remaining time for that step stays at whatever was left (session override), unless we later decide to snap back to nominal — default is keep remaining.
|
||||
- Esc **before** Start stays “cancel arm”, not Stop.
|
||||
- Short beep on Stop and on Resume. The current 10-beep end-of-step alarm becomes a non-blocking audio request on `StepComplete`.
|
||||
|
||||
Esc must work in `StepRunning`, `StepStopped` (as Back), and during the complete-alarm.
|
||||
|
||||
## Tasks, cores, interrupts
|
||||
|
||||
Watchdogs stay **on**. Every long-lived task calls `esp_task_wdt_reset()` or is not on the TWDT.
|
||||
|
||||
| Task | Priority (relative) | Work |
|
||||
| --- | --- | --- |
|
||||
| `input_task` | high | Keypad scan ~20–50 Hz; GT911 read on INT or poll. Debounce. Map to commands. |
|
||||
| `ui_task` | mid | Consume events, draw dirty regions only. Never `delay` for motor or OneWire. |
|
||||
| `machine_task` | mid-high | State machine, timers via `vTaskDelayUntil` / `esp_timer`. |
|
||||
| `motor_task` | high | Agitation pattern. Waits on notifications, not `vTaskDelete`. |
|
||||
| `temp_task` | low | DS18B20: kick conversion, wait conversion time, publish `TempUpdated`. |
|
||||
| `audio_task` | low | Short beeps / alarm pattern from a queue. |
|
||||
|
||||
Arduino `loop()` becomes a thin idle (or is not used). `setup()` only starts IDF tasks.
|
||||
|
||||
### Stop path (must be low latency)
|
||||
|
||||
1. Keypad Esc or GT911 hit-test on STOP is recognized in `input_task` or, for the matrix Esc line if we later wire a dedicated GPIO, in a GPIO ISR.
|
||||
2. `Stop` is queued to `machine_task` **and** `hal_motor_request_stop()` runs:
|
||||
- set an atomic `stop_req`
|
||||
- drive **EN HIGH immediately** (driver off)
|
||||
- `xTaskNotify` the motor task to abandon the current move
|
||||
3. Motor task leaves `stepper.run()` at the next loop check (or aborts the RMT transaction in the IDF driver) and parks.
|
||||
4. Machine publishes `StepStopped` with remaining time. UI shows Resume / Back.
|
||||
|
||||
EN-off is the safety action. It does not need a new motor-driver IC.
|
||||
|
||||
### Question 8, resolved: RMT / MCPWM vs AccelStepper
|
||||
|
||||
No extra power electronics. The existing STEP/DIR driver stays.
|
||||
|
||||
Today AccelStepper **busy-polls** `stepper.run()` on a CPU core to generate step edges. That works, but:
|
||||
|
||||
- Stop latency is “next poll of `distanceToGo`”, and `vTaskDelete` is unsafe mid-pulse.
|
||||
- The core cannot sleep; jitter depends on competing work.
|
||||
|
||||
ESP32 **RMT** (or MCPWM + a step counter) can emit the pulse train in hardware. The CPU only queues “N steps this direction”. Stop then is: abort the RMT TX + drop EN, which is cleaner.
|
||||
|
||||
**Phase 1:** keep AccelStepper behind `hal_motor`, but add `stop_req` checks inside the inner `run()` loop and never `vTaskDelete` the task.
|
||||
**Phase 2 (debt burn-down):** replace AccelStepper with an IDF RMT stepper. Same `hal_motor` API.
|
||||
|
||||
## HAL sketches
|
||||
|
||||
```c
|
||||
void hal_motor_init(void);
|
||||
void hal_motor_enable(bool on);
|
||||
void hal_motor_request_stop(void); // ISR-safe: EN off + flag
|
||||
esp_err_t hal_motor_move_revs(float revs, int dir, uint32_t rpm);
|
||||
bool hal_motor_is_busy(void);
|
||||
|
||||
void hal_temp_init(void);
|
||||
esp_err_t hal_temp_read_c(float *out); // blocking OK — temp_task only
|
||||
|
||||
typedef enum { UI_KEY, UI_TOUCH } ui_src_t;
|
||||
void hal_input_init(void);
|
||||
bool hal_input_pop(ui_raw_event_t *ev);
|
||||
|
||||
void hal_display_init(void);
|
||||
void hal_display_clear(void);
|
||||
void hal_display_text(int col, int row, const char *s); // 2004 mapping
|
||||
void hal_display_flush(void); // no-op on 2004
|
||||
|
||||
void hal_audio_beep_short(void);
|
||||
void hal_audio_alarm_complete(void);
|
||||
```
|
||||
|
||||
Colour UI implements a richer draw API *or* LVGL flush, still inside `hal_display` / `app_ui`, never inside `app_machine`.
|
||||
|
||||
## UI strategy: 2004 now, touch without a rewrite
|
||||
|
||||
`app_ui` is a set of **screens** (`ProgramSelect`, `StepSelect`, `Armed`, `Running`, `Stopped`) that read the last event snapshot and emit commands.
|
||||
|
||||
- **WROOM adapter:** screens print 20×4 strings. Input adapter translates `'1'`…`'X'` into commands.
|
||||
- **S3 adapter:** same screens, different renderer.
|
||||
|
||||
### LVGL vs custom (recommendation)
|
||||
|
||||
The JC4827W543 community stack is Arduino_GFX + LVGL + TAMC_GT911 / TouchLib. LVGL will render a pretty 480×272 panel and is already demoed on this exact board.
|
||||
|
||||
For AutoFilm the visible surface is small: six programmes, a step list, one run view (name / remaining / temp), Stop / Resume / Back. That is a few dirty-rectangle updates per second.
|
||||
|
||||
**Recommendation: custom screens first, LVGL optional later.**
|
||||
|
||||
- Custom keeps the UI task tiny, avoids 4 MB-flash + PSRAM fight with LVGL v8/v9 buffers, and does not force Arduino_GFX deeper into the tree.
|
||||
- LVGL widgets are worth it when we have graphs, profile editors, or multi-language layouts. Those are out of scope.
|
||||
- If a first S3 prototype is faster with vendor LVGL demos, isolate LVGL in `board_jc4827w543` + `app_ui` and **do not** put `lv_*` types in `ui_cmd` or `app_machine`.
|
||||
- Either way: LVGL `flush_cb` and touch read run on `ui_task` / `input_task` only; no `lv_timer_handler()` on `machine_task`.
|
||||
|
||||
Touch STOP is a hit-test that emits the same `Stop` command as Esc.
|
||||
|
||||
## Process data
|
||||
|
||||
Keep the six recipes and their numeric defaults. The storage shape may change.
|
||||
|
||||
Proposed:
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
const char *name;
|
||||
uint8_t step_count;
|
||||
const process_step_t *steps; /* flash / const */
|
||||
} process_def_t;
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
uint16_t time_s;
|
||||
float cw_revs;
|
||||
float ccw_revs;
|
||||
float temp_min_c;
|
||||
float temp_pref_c;
|
||||
float temp_max_c;
|
||||
} process_step_t;
|
||||
```
|
||||
|
||||
Session overlay: `int16_t time_delta_s[step]` in RAM (L/R ±5). Not persisted. NVS profile slot is a later increment behind `app_process` (`process_save_custom()`, etc.). Custom remains the 4×10 s stub.
|
||||
|
||||
`processName[7]` becomes a pointer; `"B&WREV"` no longer depends on a 6-char cap.
|
||||
|
||||
## Audio
|
||||
|
||||
`tone()` + `delay()` moves into `audio_task`. Machine sends `AUDIO_BEEP_SHORT` or `AUDIO_ALARM_COMPLETE`. Stop must pre-empt an in-progress alarm.
|
||||
|
||||
## Safety and timing
|
||||
|
||||
- Motor EN is safe-state HIGH (disabled) at boot and on any `Fault` or `Stop`.
|
||||
- Step deadline is `esp_timer` one-shot, not a `millis()` busy loop.
|
||||
- Temp offset `+0.4 °C` stays a board/config constant.
|
||||
- Re-enable TWDT; conversion wait lives in `temp_task`.
|
||||
- No `vTaskDelete` of worker tasks after init. Tasks block on queues/notifications for life.
|
||||
|
||||
## Migration plan
|
||||
|
||||
Executable session phases: `docs/megaplans/REFACTOR-MEGAPLAN.md`.
|
||||
|
||||
1. **Docs + conventions** (P00, this branch).
|
||||
2. **IDF skeleton** (P01) Arduino-free, two sdkconfigs.
|
||||
3. **`ui_cmd` + `app_process` + golden tests** (P02).
|
||||
4. **`app_machine` + stub HAL** (P03). Host tests become the gate.
|
||||
5. **Gitea Actions** (P04) `.gitea/workflows/ci.yml`.
|
||||
6. **WROOM motion HAL** (P05) then **UI cutover** (P06) including Stop/Resume.
|
||||
7. **S3 display+touch** (P07).
|
||||
8. **Arduino debt burn-down** (P08).
|
||||
|
||||
## Acceptance checks for the first behavioural cut (WROOM)
|
||||
|
||||
- Defaults for all six processes match `docs/CURRENT_STATE.md` tables.
|
||||
- Esc during a running step disables the motor within one step-pulse loop iteration and shows Resume / Back.
|
||||
- Resume continues remaining time; Back returns to step select.
|
||||
- Short beep on Stop and Resume; complete-alarm does not block Esc.
|
||||
- Temperature updates while running without stalling remaining-time display.
|
||||
- UI remains navigable if the DS18B20 is disconnected (`--` as today).
|
||||
- Watchdog no longer disabled as a matter of policy.
|
||||
@@ -0,0 +1,83 @@
|
||||
# REFACTOR MEGAPLAN — AutoFilm-ESP32 IDF modular UI
|
||||
|
||||
Audience: coding agent. One phase = one session. Do not start the next phase in the same session.
|
||||
|
||||
## Protocol (every session)
|
||||
|
||||
1. `git checkout refactor/esp-idf-modular-ui && git pull --ff-only`
|
||||
2. Read only: `AGENTS.md`, this file (status table), **the assigned phase file**. Open `docs/TARGET_ARCHITECTURE.md` / `docs/CURRENT_STATE.md` / `docs/CICD.md` only if the phase `READ:` list says so.
|
||||
3. Execute `IN` only. Honour `OUT` and `FORBIDDEN`.
|
||||
4. Run `VERIFY` exactly. Do not push if any verify item fails.
|
||||
5. Commits: messages listed in the phase. Imperative. No secret tokens.
|
||||
6. Push `origin refactor/esp-idf-modular-ui`. PR target is `develop`, not `main`.
|
||||
7. Set phase `STATUS:` to `DONE` in the phase file **and** this table. One-line `Notes` if you diverged (API name only — do not silently change behaviour).
|
||||
|
||||
If blocked: stop, commit nothing broken, write `BLOCKED:` at top of the phase file with the exact error.
|
||||
|
||||
## Frozen constraints (never reinterpret)
|
||||
|
||||
- Recipe **values** (times s, CW, CCW, temp min/pref/max) for C41 E6 ECN-2 B&W Custom B&WREV = `src/devSequence.cpp` / `docs/CURRENT_STATE.md`. Changing values requires explicit user order + doc update same commit.
|
||||
- Stop while running: Esc `'X'` (keypad r4c4) and later touch STOP. Immediate motor **EN HIGH** (disabled). Then Resume or ReturnToStepSelect. Short beep Stop and Resume.
|
||||
- UI never blocks on motor, OneWire, or `tone()`.
|
||||
- No Arduino types in `ui_cmd`, `app_process`, `app_machine` public headers.
|
||||
- Watchdog stays **on**. No `vTaskDelete` of long-lived workers. No `vTaskDelete` of motor task.
|
||||
- Session time edits ±5 s RAM only. No NVS profiles. No Custom editor. No pumps/valves/heaters (command IDs reserved only).
|
||||
- `auto_advance` default **false** (after StepComplete wait for Start). Do not restore silent auto-chain unless a later phase says so.
|
||||
|
||||
## Branch
|
||||
|
||||
`refactor/esp-idf-modular-ui` → PR → `develop`. Rebase onto `develop` if requested; do not retarget `main`.
|
||||
|
||||
## Status
|
||||
|
||||
| ID | File | Session goal | STATUS |
|
||||
| --- | --- | --- | --- |
|
||||
| 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 | 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 |
|
||||
| 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
|
||||
|
||||
```
|
||||
P00 → P01 → P02 → P03 → P04
|
||||
↘ P05 → P06 → P07 → P08
|
||||
P04 may start after P01 (hashFiles guards) but must not be required-on-develop until P03 VERIFY is green.
|
||||
P05 requires P03 APIs. P06 requires P05. P07 requires P06 screen contract. P08 requires P06 behaviour-identical.
|
||||
```
|
||||
|
||||
## Tree (end state; create only what the current phase lists)
|
||||
|
||||
```
|
||||
main/ IDF entry, spawn tasks
|
||||
components/ui_cmd/ cmds/events C structs
|
||||
components/app_process/ const recipes + session overlay
|
||||
components/app_machine/ state machine
|
||||
components/app_ui/ screens → commands
|
||||
components/hal_{motor,temp,input,display,audio}/
|
||||
components/board_{wroom,jc4827w543}/
|
||||
tests/host/ CMake+CTest, stub HAL
|
||||
.gitea/workflows/ci.yml
|
||||
sdkconfig.defaults sdkconfig.wroom sdkconfig.s3
|
||||
src/ include/ legacy; shrink after P06
|
||||
```
|
||||
|
||||
## IDF pin
|
||||
|
||||
`espressif/idf:v5.4`. Bump only in P04/CICD together.
|
||||
|
||||
## Do not
|
||||
|
||||
- Expand scope (LVGL, NVS profiles, pumps, heater, PlatformIO CI).
|
||||
- Disable TWDT.
|
||||
- Add Arduino libraries beyond AccelStepper, Keypad, LiquidCrystal_I2C, OneWire, DallasTemperature (P05–P07 only).
|
||||
- Commit `sdkconfig` (generated), `build/`, tokens.
|
||||
@@ -0,0 +1,16 @@
|
||||
# P00 — Docs lock
|
||||
|
||||
STATUS: DONE
|
||||
SESSION: already executed (commits 9558e58, d6a3399, d8fed43)
|
||||
DEPENDS: none
|
||||
|
||||
## Result
|
||||
|
||||
Present on `refactor/esp-idf-modular-ui`:
|
||||
|
||||
- `AGENTS.md`
|
||||
- `docs/CURRENT_STATE.md`
|
||||
- `docs/TARGET_ARCHITECTURE.md`
|
||||
- `docs/CICD.md`
|
||||
|
||||
Do not rewrite those unless a later phase `Notes` a real API drift. Executing agents start at **P01**.
|
||||
@@ -0,0 +1,133 @@
|
||||
# P01 — IDF dual-board skeleton
|
||||
|
||||
STATUS: BLOCKED
|
||||
BLOCKED: no IDF (IDF_PATH unset; no idf.py; no docker.io/espressif/idf:v5.4). Sources landed; builds not run; no binaries faked.
|
||||
DEPENDS: P00
|
||||
READ: `AGENTS.md`, `docs/megaplans/REFACTOR-MEGAPLAN.md`, this file
|
||||
OUT: recipes, UI, motor, Arduino-as-component, Gitea workflow, touching `src/` behaviour
|
||||
FORBIDDEN: `esp_task_wdt_deinit`, Arduino `#include`, new libraries, changing recipe files
|
||||
|
||||
## IN
|
||||
|
||||
Create a compileable ESP-IDF v5.4 app for two boards. App: init GPIO motor-EN as disabled (HIGH), log board name, idle loop `vTaskDelay`, TWDT on. No LCD yet.
|
||||
|
||||
## FILES (create)
|
||||
|
||||
```
|
||||
CMakeLists.txt
|
||||
main/CMakeLists.txt
|
||||
main/idf_component.yml # empty dependencies: {} (no Arduino yet)
|
||||
main/main.c
|
||||
components/board_wroom/CMakeLists.txt
|
||||
components/board_wroom/include/board.h
|
||||
components/board_wroom/board.c
|
||||
components/board_jc4827w543/CMakeLists.txt
|
||||
components/board_jc4827w543/include/board.h
|
||||
components/board_jc4827w543/board.c
|
||||
sdkconfig.defaults
|
||||
sdkconfig.wroom
|
||||
sdkconfig.s3
|
||||
```
|
||||
|
||||
Edit `.gitignore` append:
|
||||
|
||||
```
|
||||
build/
|
||||
sdkconfig
|
||||
sdkconfig.old
|
||||
managed_components/
|
||||
dependencies.lock
|
||||
```
|
||||
|
||||
Do not delete `platformio.ini` / `src/` / `include/`.
|
||||
|
||||
## Board CMake
|
||||
|
||||
Exactly one board component is compiled.
|
||||
|
||||
`main/CMakeLists.txt`:
|
||||
|
||||
```
|
||||
if(NOT DEFINED AUTOFILM_BOARD)
|
||||
set(AUTOFILM_BOARD wroom CACHE STRING "wroom|jc4827w543")
|
||||
endif()
|
||||
if(AUTOFILM_BOARD STREQUAL "wroom")
|
||||
set(BOARD_COMP board_wroom)
|
||||
elseif(AUTOFILM_BOARD STREQUAL "jc4827w543")
|
||||
set(BOARD_COMP board_jc4827w543)
|
||||
else()
|
||||
message(FATAL_ERROR "AUTOFILM_BOARD=${AUTOFILM_BOARD}")
|
||||
endif()
|
||||
idf_component_register(SRCS "main.c" INCLUDE_DIRS "." REQUIRES ${BOARD_COMP} driver)
|
||||
```
|
||||
|
||||
Both `board.h` **identical API**:
|
||||
|
||||
```c
|
||||
#pragma once
|
||||
#include "driver/gpio.h"
|
||||
const char *board_name(void);
|
||||
gpio_num_t board_pin_motor_en(void); /* WROOM: 27. S3: GPIO_NUM_NC until P07 maps real spare IO */
|
||||
int board_motor_en_disable_level(void); /* 1 = HIGH means disabled */
|
||||
```
|
||||
|
||||
`board_wroom`: `name="wroom"`, EN=27, disable_level=1.
|
||||
`board_jc4827w543`: `name="jc4827w543"`, EN=`GPIO_NUM_NC`, disable_level=1. If EN is NC, `main.c` skips gpio config.
|
||||
|
||||
## main.c contract
|
||||
|
||||
- `ESP_LOGI("app", "board=%s", board_name());`
|
||||
- if EN valid: `gpio_set_direction`, `gpio_set_level(pin, disable_level)` **before** any other actuator
|
||||
- do **not** call `disableWatchdogTimers` or `esp_task_wdt_deinit`
|
||||
- loop: `vTaskDelay(pdMS_TO_TICKS(1000))`
|
||||
|
||||
## sdkconfig (minimal)
|
||||
|
||||
`sdkconfig.defaults`:
|
||||
|
||||
```
|
||||
CONFIG_ESP_TASK_WDT_EN=y
|
||||
CONFIG_ESP_TASK_WDT_TIMEOUT_S=10
|
||||
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=y
|
||||
```
|
||||
|
||||
`sdkconfig.wroom`:
|
||||
|
||||
```
|
||||
CONFIG_IDF_TARGET="esp32"
|
||||
```
|
||||
|
||||
`sdkconfig.s3`:
|
||||
|
||||
```
|
||||
CONFIG_IDF_TARGET="esp32s3"
|
||||
CONFIG_SPIRAM=y
|
||||
CONFIG_SPIRAM_MODE_OCT=y
|
||||
CONFIG_SPIRAM_SPEED_80M=y
|
||||
```
|
||||
|
||||
If a Kconfig name fails on v5.4, fix to the v5.4 equivalent; do not drop PSRAM on S3.
|
||||
|
||||
## VERIFY
|
||||
|
||||
Need `IDF_PATH` or docker `espressif/idf:v5.4`.
|
||||
|
||||
```
|
||||
idf.py -B /tmp/af-wroom -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.wroom" -D AUTOFILM_BOARD=wroom set-target esp32 build
|
||||
idf.py -B /tmp/af-s3 -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.s3" -D AUTOFILM_BOARD=jc4827w543 set-target esp32s3 build
|
||||
```
|
||||
|
||||
Pass = both exit 0. If no IDF in environment: document `BLOCKED: no IDF` and still land the files; do not fake binaries.
|
||||
|
||||
## COMMITS
|
||||
|
||||
1. `Add ESP-IDF skeleton for wroom and jc4827w543 boards`
|
||||
2. (if needed) `Ignore IDF build products`
|
||||
|
||||
## DoD
|
||||
|
||||
- [ ] Both CMake board comps export the same `board.h` API
|
||||
- [ ] Motor EN disabled at boot on wroom
|
||||
- [ ] TWDT not disabled
|
||||
- [ ] Legacy Arduino tree untouched
|
||||
- [ ] STATUS→DONE
|
||||
@@ -0,0 +1,197 @@
|
||||
# P02 — ui_cmd + app_process + golden tests
|
||||
|
||||
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
|
||||
FORBIDDEN: mutate recipe numbers; `String`; Arduino headers in new public `.h`
|
||||
|
||||
## IN
|
||||
|
||||
C components + host tests that lock recipe bytes. No firmware behaviour change yet (`main.c` may log process count).
|
||||
|
||||
## FILES
|
||||
|
||||
```
|
||||
components/ui_cmd/CMakeLists.txt
|
||||
components/ui_cmd/include/ui_cmd.h
|
||||
components/app_process/CMakeLists.txt
|
||||
components/app_process/include/app_process.h
|
||||
components/app_process/app_process.c
|
||||
tests/host/CMakeLists.txt
|
||||
tests/host/test_process.c
|
||||
tests/host/main.c # if Unity/CTest needs a runner; else add_executable per test
|
||||
```
|
||||
|
||||
Host CMake **must not** require `IDF_PATH`. Link `app_process` sources directly. Provide `esp_err_t` typedef if needed:
|
||||
|
||||
```c
|
||||
typedef int esp_err_t;
|
||||
#define ESP_OK 0
|
||||
#define ESP_ERR_NOT_FOUND 0x105
|
||||
#define ESP_ERR_INVALID_ARG 0x102
|
||||
```
|
||||
|
||||
in `tests/host/esp_host_compat.h` and include it only in host builds.
|
||||
|
||||
## ui_cmd.h (lock)
|
||||
|
||||
Packed, no pointers in the union payload except none — use ids.
|
||||
|
||||
```c
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#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;
|
||||
```
|
||||
|
||||
P03+ fill unused fields with 0 / NAN. Do not add `char name[n]` to the event if it forces copies; UI looks up names via `app_process` by id+index.
|
||||
|
||||
## app_process.h (lock)
|
||||
|
||||
```c
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h" /* or host compat */
|
||||
|
||||
#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);
|
||||
```
|
||||
|
||||
Session overlay: `static int16_t time_delta_s[PROCESS_COUNT][PROCESS_MAX_STEPS]`. `adjust_time` adds delta; does **not** write `process_step_t.time_s`. Clamp resulting time to `[0, 36000]`.
|
||||
|
||||
## Golden table (assert exact)
|
||||
|
||||
Copy from `src/devSequence.cpp`. Names `strcmp`. Floats: compare with `fabsf(a-b)<1e-4` except integers as exact.
|
||||
|
||||
**C41** `name="C41"` count=7
|
||||
|
||||
| i | name | t | cw | ccw | min | pref | max |
|
||||
|---|---|---:|---:|---:|---:|---:|---:|
|
||||
| 0 | Prewarm | 180 | 1 | 1 | 37.8 | 38 | 38.2 |
|
||||
| 1 | Developer | 195 | 5.5 | 5 | 37.8 | 38 | 38.2 |
|
||||
| 2 | Bleach | 45 | 5.5 | 5 | 32 | 38 | 38.2 |
|
||||
| 3 | Fix | 180 | 5.5 | 5 | 32 | 38 | 38.2 |
|
||||
| 4 | Rinse 1 | 60 | 3.5 | 3 | 32 | 38 | 38.2 |
|
||||
| 5 | Rinse 2 | 60 | 3.5 | 3 | 32 | 38 | 38.2 |
|
||||
| 6 | Fin Rinse | 30 | 1 | 1 | 32 | 38 | 38.2 |
|
||||
|
||||
**E6** `name="E6"` 12: times `{180,360,120,120,360,120,360,240,120,120,120,30}` names `Preheat,FirstDev,Wash 1,Reversal,ColorDev,PreBleach,Bleach,Fixer,Wash 2,Wash 3,Wash 4,Fin Rinse` cw `{1,5.5,3.5,5,5.5,5.5,5.5,5.5,3.5,3.5,3.5,3.5}` ccw `{1,5,3,5.5,5,5.5,5.5,5.5,3,3,3,3}` min `{37.5,37.7,33.0,37.7,37.0,37,37.5,37.5,33.0,33.0,33.0,19.0}` pref `{38,38,38,38,38,38,38,38,38,38,38,20}` max `{38.5,38.3,38.0,38.3,39.0,38,38.5,38.5,38.5,38.5,38.5,21.0}`
|
||||
|
||||
**ECN-2** `name="ECN-2"` 9: t `{180,0,210,60,180,150,120,300,120}` names `Prebath,RemJet,Developer,Stop Bath,Wash,Bleach,Fixer,Wash 2,Fin Rinse` cw `{1,0,5.5,3.5,3.5,5.5,5.5,3.5,1}` ccw `{1,0,5,3,3,5,5,3,1}` min `{27,0,40.8,27,27,27,27,27,27}` pref `{38,0,41,38,38,38,38,38,38}` max `{38,0,41.2,38,38,38,38,38,38}`
|
||||
|
||||
**B&W** `name="B&W"` 7: t `{510,30,300,60,90,120,30}` names `Developer,Stop,Fix,Rinse 1,Rinse 2,Rinse 3,Fin Rinse` cw `{5.5,3.5,3.5,3.5,3.5,3.5,1}` ccw `{5,3,3,3,3,3,1}` min 19 pref 20 max 21 all.
|
||||
|
||||
**Custom** `name="Custom"` 4: t 10, names `Developer,Stop,Fix,Rinse` cw `{5.5,3.5,3.5,3.5}` ccw `{5,3,3,3}` min 19 pref 20 max 21.
|
||||
|
||||
**B&WREV** `name="B&WREV"` 12: t `{720,300,300,60,120,60,120,360,60,300,60,60}` names `FirstDev,Wash 1,Bleach,Wash 2,Clearing,Wash 3,Reversal,SecondDev,Wash 4,Fix,Wash 5,Fin Rinse` cw `{5.5,3.5,5.5,3.5,5.5,3.5,5.5,5.5,3.5,5.5,3.5,3.5}` ccw `{5,3,5,3,5,3,5,5,3,5,3,3}` min `{19.5,15.5,19.5,15.5,19.5,15.5,19.5,19.5,15.5,19.5,15.5,15.5}` pref 20 all max `{20.5,22.5,22.5,22.5,22.5,22.5,22.5,22.5,22.5,22.5,22.5,22.5}`
|
||||
|
||||
Tests also: `find_name("nope")==NULL`; `adjust_time(C41,1,+5)` → 200, const table still 195; `reset_session` restores 195.
|
||||
|
||||
## VERIFY
|
||||
|
||||
```
|
||||
cmake -S tests/host -B build/host && cmake --build build/host && ctest --test-dir build/host --output-on-failure
|
||||
```
|
||||
|
||||
All golden tests pass.
|
||||
|
||||
## COMMITS
|
||||
|
||||
1. `Add ui_cmd and app_process with frozen recipe tables`
|
||||
2. `Add host tests for process lookup and session time overlay`
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] Headers Arduino-free
|
||||
- [x] Every cell in golden table asserted
|
||||
- [x] Overlay does not mutate const steps
|
||||
- [x] STATUS→DONE
|
||||
@@ -0,0 +1,136 @@
|
||||
# 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
|
||||
@@ -0,0 +1,56 @@
|
||||
# P04 — Gitea Actions CI
|
||||
|
||||
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
|
||||
FORBIDDEN: GitHub-only `espressif/esp-idf-ci-action` as the compile driver; secrets in yaml; required-check flip if firmware still does not compile
|
||||
|
||||
## IN
|
||||
|
||||
Add `.gitea/workflows/ci.yml` per `docs/CICD.md` example. Use `hashFiles` guards:
|
||||
|
||||
- test job runs cmake/ctest only if `tests/host/CMakeLists.txt` exists
|
||||
- firmware job runs idf.py only if root `CMakeLists.txt` exists (P01)
|
||||
|
||||
Container: `docker.io/espressif/idf:v5.4`. `fail-fast: false`. `needs: test` on firmware.
|
||||
|
||||
Artifact names: `firmware-${{ matrix.board }}` including merged bin, bootloader, partition-table, flasher_args, sdkconfig, size txt.
|
||||
|
||||
Gitea context: try `${{ gitea.sha }}`; if runner only has `github.sha`, use that. Document which in Notes.
|
||||
|
||||
Triggers exactly:
|
||||
|
||||
```yaml
|
||||
on:
|
||||
pull_request:
|
||||
branches: [develop]
|
||||
push:
|
||||
branches: [develop, "refactor/**", "feature/**"]
|
||||
tags: ["v*"]
|
||||
workflow_dispatch:
|
||||
```
|
||||
|
||||
`runs-on: ubuntu-latest`.
|
||||
|
||||
Firmware matrix: wroom/esp32/sdkconfig.wroom and jc4827w543/esp32s3/sdkconfig.s3.
|
||||
|
||||
`git config --global --add safe.directory '*'` inside container before idf.py.
|
||||
|
||||
Do **not** add a release-attach job unless tag attach API is already used on this Gitea; skip release job in v1.
|
||||
|
||||
## VERIFY
|
||||
|
||||
- yaml parses (python `-c 'import yaml; yaml.safe_load(open(".gitea/workflows/ci.yml"))'` if PyYAML present, else visual: valid YAML, two-space indent)
|
||||
- If Gitea runner visible: push and confirm jobs queued. If not: still land file; Notes=`runner not verified`
|
||||
|
||||
## COMMITS
|
||||
|
||||
1. `Add Gitea Actions workflow for host tests and dual firmware`
|
||||
|
||||
## DoD
|
||||
|
||||
- [ ] Path `.gitea/workflows/ci.yml`
|
||||
- [ ] Guards prevent red on missing tests/host before P02
|
||||
- [ ] STATUS→DONE
|
||||
@@ -0,0 +1,107 @@
|
||||
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: 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
|
||||
FORBIDDEN: new Arduino libraries; `tone()` on machine task; blocking OneWire on UI task
|
||||
|
||||
## IN
|
||||
|
||||
Device implementations of `hal_motor`, `hal_temp`, `hal_audio` for `board_wroom`. Arduino-as-component **first allowed here**.
|
||||
|
||||
Keep AccelStepper behind `hal_motor`. Inner step loop **must** sample `stop_req` every `stepper.run()` and exit. Motor task is immortal: wait on notification for next `agitate_start`.
|
||||
|
||||
## Arduino component
|
||||
|
||||
`main/idf_component.yml`:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
espressif/arduino-esp32: "~3.1.0"
|
||||
```
|
||||
|
||||
If 3.1 conflicts with IDF 5.4, pin the combo that compiles; Notes the versions. Only WROOM firmware enables Arduino this phase; S3 may compile without motor SRCS via `if(AUTOFILM_BOARD STREQUAL wroom)`.
|
||||
|
||||
AccelStepper: add as managed component **or** vendor `components/third_party/AccelStepper` (upstream 1.64). Prefer `idf_component.yml` git dep if it builds; else vendor the .cpp/.h used today.
|
||||
|
||||
OneWire + DallasTemperature: same policy. Keypad/LCD **not** this phase.
|
||||
|
||||
## Pins (wroom only, from config)
|
||||
|
||||
| fn | gpio |
|
||||
|---|---|
|
||||
| STEP | 12 |
|
||||
| DIR | 14 |
|
||||
| EN active-low | 27 |
|
||||
| DS18B20 | 13 |
|
||||
| beep | 25 |
|
||||
|
||||
`STEPS_PER_REV=4800` `RPM=60` `ACCEL=9600` `TEMP_OFFSET=0.4f`
|
||||
|
||||
## motor_task
|
||||
|
||||
Priority high, core 0. Loop:
|
||||
|
||||
```
|
||||
wait notify
|
||||
while !stop_req && agitating:
|
||||
move CW cw_revs (run loop checks stop_req)
|
||||
if stop_req break
|
||||
move CCW ccw_revs
|
||||
hal_motor_enable(false)
|
||||
```
|
||||
|
||||
`hal_motor_request_stop`: atomic stop_req=1; `gpio_set_level(EN,1)` **immediately**; notify. ISR-safe: no heap, no stepper API.
|
||||
|
||||
Never `vTaskDelete`.
|
||||
|
||||
## temp_task
|
||||
|
||||
Low prio. Period: request conversion, `vTaskDelay(750ms)`, read, add 0.4, `app_machine_on_temp`. On disconnect `ok=false`. Do not write LCD.
|
||||
|
||||
## audio_task
|
||||
|
||||
Queue of `{SHORT, ALARM, CANCEL}`. SHORT: one 2 kHz 80–120 ms pulse (LEDC or Arduino `tone` **in this task only**). ALARM: 10× (2000 Hz 500 ms on / 250 ms off) **abort immediately on CANCEL or STOP queued**. Machine never `delay`s for sound.
|
||||
|
||||
## main.c (wroom)
|
||||
|
||||
`hal_*_init`, `app_process_init`, `app_machine_init`, create motor/temp/audio tasks. Do not start UI yet (P06). Optional: 1 Hz log remaining if RUNNING for bring-up.
|
||||
|
||||
S3 build: stub motor/temp/audio weak symbols or board-local no-ops so P01 S3 still links.
|
||||
|
||||
## VERIFY
|
||||
|
||||
Host tests still pass.
|
||||
|
||||
WROOM `idf.py` build pass.
|
||||
|
||||
Manual on hardware (if unavailable, Notes=`hw not flashed`; still land code):
|
||||
|
||||
1. Custom start (will need P06 for keypad — this phase may expose a 5 s auto-demo **behind `#ifdef AUTOFILM_MOTION_SMOKE` default off**). Do not enable smoke in production sdkconfig.
|
||||
2. Logic-analyzer optional. Minimum: compile + review EN init HIGH.
|
||||
|
||||
## COMMITS
|
||||
|
||||
1. `Add Arduino-as-component and WROOM motor HAL with cooperative stop`
|
||||
2. `Add temp and audio tasks off the UI path`
|
||||
|
||||
## DoD
|
||||
|
||||
- [ ] EN HIGH at init and on stop
|
||||
- [ ] stop_req checked inside step loop
|
||||
- [ ] OneWire only in temp_task
|
||||
- [ ] tone/LEDC only in audio_task
|
||||
- [ ] STATUS→DONE
|
||||
@@ -0,0 +1,137 @@
|
||||
# P06 — WROOM UI cutover (2004 + keypad + Stop/Resume)
|
||||
|
||||
STATUS: TODO
|
||||
DEPENDS: P05
|
||||
READ: this file, `src/menu.cpp`, `src/display.cpp`, `src/config.cpp`, `docs/TARGET_ARCHITECTURE.md` (screens + stop rules)
|
||||
OUT: S3 LVGL, NVS profiles, keeping blocking `getEntEscInput` as the live path
|
||||
FORBIDDEN: `while(true)` keypad wait in machine; `malloc` for mm:ss; `vTaskDelete`; WDT off
|
||||
|
||||
## IN
|
||||
|
||||
Replace live control path with `input_task` + `ui_task` + `app_ui` screens on 20×4. Legacy `src/*.cpp` may remain unlinked. `main` no longer calls `startingMenu()`.
|
||||
|
||||
After this phase the WROOM firmware **is** the product.
|
||||
|
||||
## FILES
|
||||
|
||||
```
|
||||
components/hal_display/include/hal_display.h
|
||||
components/hal_display/hal_display_lcd2004.cpp
|
||||
components/hal_input/include/hal_input.h
|
||||
components/hal_input/hal_input_keypad.cpp
|
||||
components/app_ui/include/app_ui.h
|
||||
components/app_ui/app_ui.c
|
||||
components/app_ui/screens.h
|
||||
```
|
||||
|
||||
C++ only in hal_display / hal_input.
|
||||
|
||||
## hal_display.h
|
||||
|
||||
```c
|
||||
void hal_display_init(void);
|
||||
void hal_display_clear(void);
|
||||
void hal_display_text(int col, int row, const char *s); /* clip to 20 cols */
|
||||
void hal_display_glyph_thermo(int col, int row);
|
||||
void hal_display_flush(void); /* no-op */
|
||||
```
|
||||
|
||||
LCD: I2C 21/22 addr 0x27, 20x4, createChar thermometer from `config.cpp` bytes.
|
||||
|
||||
mm:ss: stack buffer `char[6]`, `snprintf "%02d:%02d"`. Never malloc.
|
||||
|
||||
## hal_input.h
|
||||
|
||||
```c
|
||||
typedef struct { char key; } ui_raw_key_t;
|
||||
void hal_input_init(void);
|
||||
bool hal_input_pop_key(ui_raw_key_t *out); /* nonblocking */
|
||||
```
|
||||
|
||||
Keypad map **byte-identical** to `src/config.cpp`. Scan in `input_task` 25 Hz. Debounce via Keypad lib.
|
||||
|
||||
Map:
|
||||
|
||||
| key | cmd |
|
||||
|---|---|
|
||||
| 1..6 | SELECT_PROCESS id 0..5 |
|
||||
| U/D | BROWSE_STEP ±1 |
|
||||
| L/R | ADJUST_STEP_TIME ±5 |
|
||||
| E | ARM if STEP_SELECT; START if ARMED; RESUME if STOPPED; ARM next if COMPLETE |
|
||||
| X | CANCEL_ARMED if ARMED; STOP if RUNNING or COMPLETE; RETURN_TO_STEP_SELECT if STOPPED; ignore IDLE |
|
||||
|
||||
**X in RUNNING:** `hal_motor_request_stop()` **in input_task immediately**, then queue CMD_STOP. Dual-path required.
|
||||
|
||||
## Screens (20×4)
|
||||
|
||||
`ProgramSelect` (IDLE):
|
||||
|
||||
```
|
||||
Select Programme:
|
||||
1. C41 4. ECN-2
|
||||
2. E6 5. Custom
|
||||
3. B&W 6. B&W Rev
|
||||
```
|
||||
|
||||
`StepSelect`:
|
||||
|
||||
```
|
||||
Step Time Temp
|
||||
<name> mm:ss nnC
|
||||
Scroll / Esc / Ent
|
||||
🌡 tt.tC row3 col13 thermo+temp or --
|
||||
```
|
||||
|
||||
`Armed`: name, mm:ss, pref C, `Ent:start Esc:quit`, temp row3
|
||||
|
||||
`Running`: name row0, `Remaining: mm:ss` row1, temp row3. Update remaining ≥4 Hz from events, **not** by blocking. No `delay(410)`.
|
||||
|
||||
`Stopped`: name, remaining, `E:resume X:back`, temp row3
|
||||
|
||||
`Complete`: name `Done`, then alarm (nonblocking). `E` next / `X` back.
|
||||
|
||||
## Tasks
|
||||
|
||||
| task | prio |
|
||||
|---|---|
|
||||
| input_task | high |
|
||||
| machine_task | mid-high; `handle_cmd` + `tick(esp_timer_get_time/1000)` 50 Hz |
|
||||
| ui_task | mid; drain events, dirty draw |
|
||||
| motor/temp/audio | as P05 |
|
||||
|
||||
TWDT: register tasks that run long loops or rely on idle.
|
||||
|
||||
## Cutover
|
||||
|
||||
`main.c`: init display/input, splash `AUTOFILM` + git describe 1 s **via vTaskDelay not busy Arduino delay in ui_task after scheduler start**. Unlink `src/AutoFilmESP32.cpp` from IDF (leave files on disk).
|
||||
|
||||
`platformio.ini` may stay; do not promise PIO still builds.
|
||||
|
||||
## VERIFY
|
||||
|
||||
Host tests pass.
|
||||
|
||||
WROOM idf build pass.
|
||||
|
||||
Hardware smoke:
|
||||
|
||||
1. Select Custom, Ent to arm, Ent to start 10 s step, remaining counts, temp updates
|
||||
2. Mid-step X: motor stops at once, Stopped screen, E resumes remaining, X returns to list
|
||||
3. Disconnect DS18B20: `--` still navigable
|
||||
4. Complete alarm: X cancels and returns; UI not frozen 7.5 s
|
||||
|
||||
If no hardware: still land; Notes=`hw smoke pending`; do not skip the input dual-path stop.
|
||||
|
||||
## COMMITS
|
||||
|
||||
1. `Add 2004 display and keypad HAL`
|
||||
2. `Add app_ui screens driven by machine events`
|
||||
3. `Cut over WROOM main to RTOS UI and stop-safe input`
|
||||
|
||||
## DoD
|
||||
|
||||
- [ ] Esc during run always EN-off
|
||||
- [ ] No malloc mm:ss
|
||||
- [ ] No blocking alarm on UI/machine
|
||||
- [ ] Legacy menus not in the IDF link
|
||||
- [ ] STATUS→DONE
|
||||
@@ -0,0 +1,58 @@
|
||||
# P07 — JC4827W543 display + GT911 + STOP
|
||||
|
||||
STATUS: TODO
|
||||
DEPENDS: P06
|
||||
READ: this file, `docs/TARGET_ARCHITECTURE.md` dual-board + UI strategy, vendor pin notes below
|
||||
OUT: LVGL in `app_machine`; changing recipes; assigning WROOM STEP/DIR onto S3 without a pin table comment; pumps
|
||||
FORBIDDEN: `lv_*` types in `ui_cmd.h` / `app_machine.h`; blocking in flush_cb beyond DMA wait; copying WROOM GPIO map blindly
|
||||
|
||||
## IN
|
||||
|
||||
S3 firmware shows the **same five screens** via a colour adapter. Touch STOP = same path as Esc (`hal_motor_request_stop` + CMD_STOP). Custom draw first (no LVGL unless bring-up is otherwise blocked — if LVGL used, confine to `hal_display` + `app_ui` board files).
|
||||
|
||||
Mechatronics on S3: keep motor EN `GPIO_NUM_NC` **unless** a spare pin is documented in `board.c` comments with the header pin number. Do not invent STEP/DIR. S3 image is operator-panel-capable; agitation may remain on WROOM until pins proven.
|
||||
|
||||
## Pins (lock from community JC4827W543C)
|
||||
|
||||
Display NV3041A QSPI: CS 45, SCK 47, D0 21, D1 48, D2 40, D3 39, BL 1, 480×272.
|
||||
|
||||
GT911 I2C: SDA 8, SCL 4, INT 3, RST 38, addr 0x5D. Reset: RST low 200 ms then high; INT as input.
|
||||
|
||||
PSRAM octal 80 MHz already in `sdkconfig.s3`.
|
||||
|
||||
Free IO is scarce. Do not steal QSPI/GT911 pins for motor.
|
||||
|
||||
## FILES
|
||||
|
||||
```
|
||||
components/board_jc4827w543/board.c # update comments/pins
|
||||
components/hal_display/hal_display_nv3041a.c # or .cpp
|
||||
components/hal_input/hal_input_gt911.c
|
||||
components/app_ui/app_ui_color.c # or ifdefs in app_ui.c
|
||||
```
|
||||
|
||||
`hal_display_text` still works (glyph blit at cell grid 24×8 or similar) so `app_ui.c` can stay shared. Prefer: `app_ui` calls abstract `ui_draw_program_select()` implemented per board.
|
||||
|
||||
STOP hit-test: rectangle bottom of Running/Stopped/Complete screens, label `STOP`. On touch inside: `hal_motor_request_stop()` then CMD_STOP (same as keypad X). Resume/Back buttons on Stopped.
|
||||
|
||||
## VERIFY
|
||||
|
||||
S3 `idf.py` build pass.
|
||||
|
||||
WROOM build still pass (do not regress P06).
|
||||
|
||||
Host tests pass.
|
||||
|
||||
Hardware S3: ProgramSelect visible, tap process, Running shows name/remaining/temp, STOP stops if motor wired else still transitions UI to Stopped.
|
||||
|
||||
## COMMITS
|
||||
|
||||
1. `Add NV3041A and GT911 HAL for jc4827w543`
|
||||
2. `Map colour screens and STOP hit-test to existing commands`
|
||||
|
||||
## DoD
|
||||
|
||||
- [ ] Same commands as keypad
|
||||
- [ ] No lv types in machine/process/cmd headers
|
||||
- [ ] WROOM firmware unchanged in behaviour
|
||||
- [ ] STATUS→DONE
|
||||
@@ -0,0 +1,40 @@
|
||||
# P08 — Arduino debt burn-down
|
||||
|
||||
STATUS: TODO
|
||||
DEPENDS: P06 (behaviour parity). P07 optional.
|
||||
READ: this file, `components/hal_motor/**`, HAL headers from P03
|
||||
OUT: recipe edits; UI redesign; LVGL-by-default
|
||||
FORBIDDEN: behaviour change of times/stop/resume; dropping cooperative stop
|
||||
|
||||
## IN
|
||||
|
||||
Replace Arduino libs with IDF peripherals **behind the same HAL**. Remove Arduino-as-component from the S3 image first, then WROOM.
|
||||
|
||||
Order (one commit each, tests+builds green after each):
|
||||
|
||||
1. **Audio:** LEDC / `ledc_timer` tones; delete `tone()`.
|
||||
2. **Motor:** RMT TX pulse train for STEP, GPIO DIR/EN. `hal_motor_request_stop` aborts RMT + EN HIGH. Delete AccelStepper. Same 4800 steps/rev, 60 RPM, accel ≈9600 (RMT may use a short ramp table).
|
||||
3. **Temp:** RMT or `onewire_bus` IDF component; keep +0.4 °C offset. Delete Dallas/OneWire Arduino.
|
||||
4. **Keypad:** GPIO matrix scan in `hal_input` (no Keypad lib). Same keymap.
|
||||
5. **LCD 2004:** IDF I2C + HD44780 nibble commands (or `esp_lcd` panel io i2c). Delete LiquidCrystal_I2C.
|
||||
6. Drop `espressif/arduino-esp32` from `idf_component.yml` when nothing includes Arduino.h.
|
||||
|
||||
Do not change `hal_*` public headers except adding RMT-specific internals in `.c`.
|
||||
|
||||
## VERIFY
|
||||
|
||||
Host tests pass (stubs unchanged).
|
||||
|
||||
Both boards build without Arduino component.
|
||||
|
||||
Hardware WROOM: Custom 10 s, Esc mid-step EN off, Resume remaining, temp display, beep Stop/Resume, complete alarm cancellable.
|
||||
|
||||
## COMMITS
|
||||
|
||||
Per numbered item above, e.g. `Replace AccelStepper with RMT step pulse HAL`.
|
||||
|
||||
## DoD
|
||||
|
||||
- [ ] `rg -n "Arduino.h|AccelStepper|LiquidCrystal|DallasTemperature|#include <Keypad.h>|#include <OneWire.h>"` empty in `main/` `components/`
|
||||
- [ ] Stop still EN-off first
|
||||
- [ ] STATUS→DONE
|
||||
@@ -0,0 +1,11 @@
|
||||
if(NOT DEFINED AUTOFILM_BOARD)
|
||||
set(AUTOFILM_BOARD wroom CACHE STRING "wroom|jc4827w543")
|
||||
endif()
|
||||
if(AUTOFILM_BOARD STREQUAL "wroom")
|
||||
set(BOARD_COMP board_wroom)
|
||||
elseif(AUTOFILM_BOARD STREQUAL "jc4827w543")
|
||||
set(BOARD_COMP board_jc4827w543)
|
||||
else()
|
||||
message(FATAL_ERROR "AUTOFILM_BOARD=${AUTOFILM_BOARD}")
|
||||
endif()
|
||||
idf_component_register(SRCS "main.c" INCLUDE_DIRS "." REQUIRES ${BOARD_COMP} driver)
|
||||
@@ -0,0 +1 @@
|
||||
dependencies: {}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
#include "board.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
static const char *TAG = "app";
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "board=%s", board_name());
|
||||
|
||||
gpio_num_t en_pin = board_pin_motor_en();
|
||||
if (en_pin != GPIO_NUM_NC) {
|
||||
gpio_config_t io = {
|
||||
.pin_bit_mask = 1ULL << en_pin,
|
||||
.mode = GPIO_MODE_OUTPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
gpio_config(&io);
|
||||
gpio_set_level(en_pin, board_motor_en_disable_level());
|
||||
}
|
||||
|
||||
while (1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
CONFIG_ESP_TASK_WDT_EN=y
|
||||
CONFIG_ESP_TASK_WDT_TIMEOUT_S=10
|
||||
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=y
|
||||
@@ -0,0 +1,4 @@
|
||||
CONFIG_IDF_TARGET="esp32s3"
|
||||
CONFIG_SPIRAM=y
|
||||
CONFIG_SPIRAM_MODE_OCT=y
|
||||
CONFIG_SPIRAM_SPEED_80M=y
|
||||
@@ -0,0 +1 @@
|
||||
CONFIG_IDF_TARGET="esp32"
|
||||
@@ -0,0 +1,46 @@
|
||||
cmake_minimum_required(VERSION 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
|
||||
${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)
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
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
|
||||
@@ -0,0 +1,6 @@
|
||||
int test_process_run(void);
|
||||
|
||||
int main(void)
|
||||
{
|
||||
return test_process_run();
|
||||
}
|
||||
@@ -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++;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#include "hal_temp.h"
|
||||
#include <stddef.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
#include "app_machine.h"
|
||||
#include "app_process.h"
|
||||
#include "hal_motor.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
#include "app_process.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user