Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b5c7a096f | ||
|
|
8f5b41ebba | ||
|
|
c047d5d96f | ||
|
|
461771cc02 | ||
|
|
9b2c6bf3ee | ||
|
|
415c13a605 | ||
|
|
d8397778d5 | ||
|
|
255db87881 | ||
|
|
179efb54cd | ||
|
|
01cc76b8b1 | ||
|
|
a9298ae521 | ||
|
|
2458a61625 | ||
|
|
d8fed43b42 |
@@ -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/c_cpp_properties.json
|
||||||
.vscode/launch.json
|
.vscode/launch.json
|
||||||
.vscode/ipch
|
.vscode/ipch
|
||||||
|
build/
|
||||||
|
sdkconfig
|
||||||
|
sdkconfig.old
|
||||||
|
managed_components/
|
||||||
|
dependencies.lock
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ Instructions for humans and coding agents working in this repository.
|
|||||||
- Integration branch is **`develop`**. Feature and refactor work is done on a branch cut from `develop`, then opened as a PR **into `develop`**.
|
- 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.
|
- 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`).
|
- 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`. Update those docs when the design changes; do not leave the tree contradicting them.
|
- 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).
|
- Do not commit secrets (Git tokens, Wi-Fi passwords, `sdkconfig` with provisioned keys).
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
@@ -27,6 +28,7 @@ Instructions for humans and coding agents working in this repository.
|
|||||||
| `components/hal_*` | Hardware adapters |
|
| `components/hal_*` | Hardware adapters |
|
||||||
| `components/board_*` | Pin maps and board bring-up |
|
| `components/board_*` | Pin maps and board bring-up |
|
||||||
| `docs/` | Design notes |
|
| `docs/` | Design notes |
|
||||||
|
| `.gitea/workflows/` | Gitea Actions (tests + firmware builds) |
|
||||||
|
|
||||||
New behaviour goes into `components/`, not into larger `menu.cpp` files.
|
New behaviour goes into `components/`, not into larger `menu.cpp` files.
|
||||||
|
|
||||||
@@ -85,5 +87,7 @@ New behaviour goes into `components/`, not into larger `menu.cpp` files.
|
|||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
- Prefer host-side tests for `app_process` lookup and the machine state machine with a stub motor.
|
- 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.
|
- 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.
|
||||||
@@ -41,6 +41,8 @@ idf.py -D AUTOFILM_BOARD=wroom @sdkconfig.defaults @sdkconfig.wroom
|
|||||||
idf.py -D AUTOFILM_BOARD=jc4827w543 @sdkconfig.defaults @sdkconfig.s3
|
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.
|
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.
|
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.
|
||||||
@@ -284,13 +286,16 @@ Session overlay: `int16_t time_delta_s[step]` in RAM (L/R ±5). Not persisted. N
|
|||||||
|
|
||||||
## Migration plan
|
## Migration plan
|
||||||
|
|
||||||
1. **Docs + conventions** (this branch, this commit).
|
Executable session phases: `docs/megaplans/REFACTOR-MEGAPLAN.md`.
|
||||||
2. **IDF skeleton** with Arduino component, two sdkconfigs, both boards compile a blink/splash.
|
|
||||||
3. **`ui_cmd` + machine state machine** with a fake motor (log + delay) and existing recipes copied 1:1.
|
1. **Docs + conventions** (P00, this branch).
|
||||||
4. **HAL wrap** of current AccelStepper / LCD / keypad / DS18B20 / beeper on WROOM. Feature-complete vs today **plus Stop/Resume**.
|
2. **IDF skeleton** (P01) Arduino-free, two sdkconfigs.
|
||||||
5. **Delete blocking menus** (`startingMenu` / `getEntEscInput` busy loops).
|
3. **`ui_cmd` + `app_process` + golden tests** (P02).
|
||||||
6. **S3 display+touch adapter** showing the same screens; STOP hit-test wired to `Stop`.
|
4. **`app_machine` + stub HAL** (P03). Host tests become the gate.
|
||||||
7. **Burn-down:** RMT stepper, IDF I²C LCD or drop 2004, IDF OneWire or RMT 1-Wire, IDF LEDC tones; remove Arduino component from the S3 image first.
|
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)
|
## Acceptance checks for the first behavioural cut (WROOM)
|
||||||
|
|
||||||
|
|||||||
@@ -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