|
| 1 | +# Task: Preflight build, tests, and boot-loop guard for M5Tab5-UserDemo |
| 2 | + |
| 3 | +## Objective |
| 4 | +When Codex (or any contributor) proposes code: |
| 5 | +1) Use repo tools (esp. `tools/install_idf.sh`) to prepare ESP-IDF. |
| 6 | +2) Build exactly like the maintainer does. |
| 7 | +3) Run host/unit tests in `tests/` and fail if any test fails. |
| 8 | +4) If a serial port is available, run a boot-loop guard immediately after flashing on a local/dev machine (CI step is optional unless a hardware runner is present). |
| 9 | +5) Only open PRs that **pass preflight**. |
| 10 | + |
| 11 | +## Canonical local build sequence (MUST be followed) |
| 12 | +```bash |
| 13 | +git clone https://github.com/baba-dev/M5Tab5-UserDemo |
| 14 | +cd M5Tab5-UserDemo |
| 15 | + |
| 16 | +# Install / activate IDF via our repo tool (no ad-hoc installers) |
| 17 | +bash tools/install_idf.sh # sets/exports IDF env for this shell |
| 18 | + |
| 19 | +# Fetch external deps (must succeed) |
| 20 | +python ./fetch_repos.py |
| 21 | + |
| 22 | +# Build for Tab5 |
| 23 | +cd platforms/tab5 |
| 24 | +idf.py set-target esp32p4 |
| 25 | +idf.py build |
| 26 | +``` |
| 27 | + |
| 28 | +Use idf.py set-target esp32p4 (this clears & regenerates sdkconfig as per IDF docs). |
| 29 | +Espressif Systems |
| 30 | + |
| 31 | +Don’t invent alternative flows or other actions to “speed up”; this path is authoritative. The M5Stack Tab5 user demo guide also shows the same platforms/tab5 layout and fetch_repos.py. |
| 32 | +M5Stack Docs |
| 33 | + |
| 34 | +Tests (MUST PASS) |
| 35 | + |
| 36 | +Run component/unit tests (Unity) discovered under tests/ or component test/ folders. Unity test layout is the official way for ESP-IDF. |
| 37 | +Espressif Systems |
| 38 | + |
| 39 | +If a host CMake test project exists, build it and run ctest with --output-on-failure. |
| 40 | + |
| 41 | +On hardware test rigs (optional), use pytest-embedded to drive simple on-target checks. |
| 42 | +Espressif Systems |
| 43 | ++1 |
| 44 | + |
| 45 | +Boot-loop guard (MUST if serial available; optional in CI) |
| 46 | + |
| 47 | +After flashing on a developer machine, run scripts/bootloop_guard.py with --port <PORT> for 30–45s. |
| 48 | + |
| 49 | +Fail if monitor shows repeating reset banners/panics (e.g., rst:, panic, abort, watchdog), or >3 resets in the window. Panic/cores are standard IDF behaviors. |
| 50 | +Espressif Systems |
| 51 | ++2 |
| 52 | +Espressif Systems |
| 53 | ++2 |
| 54 | + |
| 55 | +Acceptance criteria (PRs that Codex proposes MUST meet all) |
| 56 | + |
| 57 | +Builds with idf.py set-target esp32p4 && idf.py build under platforms/tab5 with the IDF env from tools/install_idf.sh. |
| 58 | +Espressif Systems |
| 59 | + |
| 60 | +python ./fetch_repos.py completes without errors (deps present). |
| 61 | + |
| 62 | +./scripts/preflight.sh exits 0 locally; CI job idf-preflight is green. |
| 63 | + |
| 64 | +If SERIAL_PORT is set or --port provided, bootloop_guard.py reports NO_BOOTLOOP. |
| 65 | + |
| 66 | +All tests in tests/ (and component test/) pass (Unity/pytest). |
| 67 | +Espressif Systems |
| 68 | ++1 |
| 69 | + |
| 70 | +Notes |
| 71 | + |
| 72 | +Use IDF monitor semantics and patterns for parsing the serial stream (we rely on standard IDF monitor output). |
| 73 | +Espressif Systems |
| 74 | + |
| 75 | +Do not downgrade/replace IDF. Use our installer script and the IDF it selects. |
| 76 | + |
| 77 | + |
| 78 | +--- |
| 79 | + |
| 80 | +# Add these helper files |
| 81 | + |
| 82 | +## 1) `scripts/preflight.sh` (bash) |
| 83 | + |
| 84 | +```bash |
| 85 | +#!/usr/bin/env bash |
| 86 | +set -euo pipefail |
| 87 | + |
| 88 | +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" |
| 89 | +cd "$ROOT_DIR" |
| 90 | + |
| 91 | +echo "[preflight] Using repo installer to setup ESP-IDF..." |
| 92 | +bash tools/install_idf.sh |
| 93 | + |
| 94 | +echo "[preflight] Fetching external components..." |
| 95 | +python ./fetch_repos.py |
| 96 | + |
| 97 | +echo "[preflight] Building Tab5 app (esp32p4)..." |
| 98 | +pushd platforms/tab5 >/dev/null |
| 99 | +idf.py set-target esp32p4 |
| 100 | +idf.py build |
| 101 | +popd >/dev/null |
| 102 | + |
| 103 | +echo "[preflight] Running host/unit tests if present..." |
| 104 | +if [ -f tests/CMakeLists.txt ]; then |
| 105 | + cmake -S tests -B build/tests -DCMAKE_BUILD_TYPE=Debug |
| 106 | + cmake --build build/tests -- -j |
| 107 | + ctest --test-dir build/tests --output-on-failure |
| 108 | +fi |
| 109 | + |
| 110 | +# Component Unity tests via IDF (if any component has test/ with Unity cases) |
| 111 | +if grep -R --include="CMakeLists.txt" -n "idf_component_register" components >/dev/null 2>&1; then |
| 112 | + echo "[preflight] Scanning for Unity tests in components/*/test ..." |
| 113 | + # IDF standard: tests usually land in a dedicated test app; if one exists, build it. |
| 114 | + # If you have a test app target (e.g., 'unit_test_app'), you can uncomment: |
| 115 | + # pushd platforms/tab5 >/dev/null |
| 116 | + # idf.py build -DIDF_UNIT_TESTING=1 |
| 117 | + # popd >/dev/null |
| 118 | +fi |
| 119 | + |
| 120 | +# Optional hardware boot-loop guard |
| 121 | +if [ "${SERIAL_PORT:-}" != "" ]; then |
| 122 | + echo "[preflight] Serial port detected ($SERIAL_PORT); running boot-loop guard..." |
| 123 | + python scripts/bootloop_guard.py --port "$SERIAL_PORT" --seconds 45 || { |
| 124 | + echo "::error::Boot-loop or panic detected on device" |
| 125 | + exit 1 |
| 126 | + } |
| 127 | +fi |
| 128 | + |
| 129 | +echo "[preflight] OK" |
| 130 | +``` |
| 131 | + |
| 132 | +Why these choices? |
| 133 | + |
| 134 | +idf.py is the canonical front-end for build/flash/monitor. |
| 135 | +Espressif Systems |
| 136 | + |
| 137 | +set-target explicitly selects esp32p4 and regenerates config as documented. |
| 138 | +Espressif Systems |
| 139 | + |
| 140 | +## 2) scripts/bootloop_guard.py |
| 141 | +``` |
| 142 | +#!/usr/bin/env python3 |
| 143 | +import argparse, sys, time, re |
| 144 | +try: |
| 145 | + import serial |
| 146 | +except ImportError: |
| 147 | + print("pyserial not installed. pip install pyserial", file=sys.stderr) |
| 148 | + sys.exit(2) |
| 149 | +
|
| 150 | +PATTERNS = [ |
| 151 | + re.compile(rb"rst:0x", re.I), |
| 152 | + re.compile(rb"panic", re.I), |
| 153 | + re.compile(rb"abort", re.I), |
| 154 | + re.compile(rb"Guru Meditation", re.I), # legacy wording still appears on some targets |
| 155 | + re.compile(rb"watchdog", re.I), |
| 156 | +] |
| 157 | +
|
| 158 | +def main(): |
| 159 | + ap = argparse.ArgumentParser() |
| 160 | + ap.add_argument("--port", required=True, help="Serial port (e.g. COM5 or /dev/ttyACM0)") |
| 161 | + ap.add_argument("--baud", type=int, default=115200) |
| 162 | + ap.add_argument("--seconds", type=int, default=30) |
| 163 | + args = ap.parse_args() |
| 164 | +
|
| 165 | + resets = 0 |
| 166 | + start = time.time() |
| 167 | +
|
| 168 | + try: |
| 169 | + with serial.Serial(args.port, args.baud, timeout=0.2) as ser: |
| 170 | + ser.reset_input_buffer() |
| 171 | + while time.time() - start < args.seconds: |
| 172 | + data = ser.read(8192) |
| 173 | + if not data: |
| 174 | + continue |
| 175 | + sys.stdout.buffer.write(data) |
| 176 | + sys.stdout.flush() |
| 177 | + for pat in PATTERNS: |
| 178 | + if pat.search(data): |
| 179 | + if b"rst:" in data: |
| 180 | + resets += 1 |
| 181 | + else: |
| 182 | + print("\n[bootloop_guard] panic/abort/watchdog matched -> FAIL", file=sys.stderr) |
| 183 | + return 1 |
| 184 | + if resets > 3: |
| 185 | + print("\n[bootloop_guard] too many resets -> FAIL", file=sys.stderr) |
| 186 | + return 1 |
| 187 | + except serial.SerialException as e: |
| 188 | + print(f"[bootloop_guard] Serial error: {e}", file=sys.stderr) |
| 189 | + return 2 |
| 190 | +
|
| 191 | + print("\n[bootloop_guard] NO_BOOTLOOP", file=sys.stderr) |
| 192 | + return 0 |
| 193 | +
|
| 194 | +if __name__ == "__main__": |
| 195 | + sys.exit(main()) |
| 196 | +``` |
| 197 | + |
| 198 | +This follows IDF monitor semantics (scan reset banner/panic strings). For reference: idf.py monitor behavior & fatal error/panic docs. |
| 199 | +Espressif Systems |
| 200 | ++1 |
| 201 | + |
| 202 | +Make it executable: |
| 203 | + |
| 204 | +git update-index --add --chmod=+x scripts/preflight.sh |
| 205 | + |
| 206 | +CI that enforces the same thing |
| 207 | + |
| 208 | +Path: .github/workflows/idf-preflight.yml |
| 209 | + |
| 210 | +``` |
| 211 | +name: IDF Preflight (esp32p4) |
| 212 | +
|
| 213 | +on: |
| 214 | + push: |
| 215 | + pull_request: |
| 216 | +
|
| 217 | +jobs: |
| 218 | + build: |
| 219 | + runs-on: ubuntu-latest |
| 220 | + steps: |
| 221 | + - name: Checkout |
| 222 | + uses: actions/checkout@v4 |
| 223 | +
|
| 224 | + # Install Python deps (pytest-embedded optional; enables on-target tests when HW is present) |
| 225 | + - name: Python setup |
| 226 | + uses: actions/setup-python@v5 |
| 227 | + with: |
| 228 | + python-version: "3.11" |
| 229 | + - run: | |
| 230 | + python -m pip install --upgrade pip |
| 231 | + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi |
| 232 | + pip install pyserial pytest pytest-embedded pytest-embedded-idf |
| 233 | + shell: bash |
| 234 | +
|
| 235 | + # Install ESP-IDF using official action (respects Linux runners) |
| 236 | + - name: Install ESP-IDF |
| 237 | + uses: espressif/install-esp-idf-action@v1 |
| 238 | + with: |
| 239 | + version: latest |
| 240 | + # Alternatively, to match your repo script exactly, source your installer: |
| 241 | + # - run: bash tools/install_idf.sh |
| 242 | +
|
| 243 | + - name: Preflight |
| 244 | + shell: bash |
| 245 | + run: | |
| 246 | + bash scripts/preflight.sh |
| 247 | +
|
| 248 | + # Upload build artifacts & logs for debugging |
| 249 | + - name: Archive build |
| 250 | + if: always() |
| 251 | + uses: actions/upload-artifact@v4 |
| 252 | + with: |
| 253 | + name: tab5-build |
| 254 | + path: | |
| 255 | + platforms/tab5/build/** |
| 256 | + **/build/tests/** |
| 257 | + if-no-files-found: ignore |
| 258 | +``` |
| 259 | + |
| 260 | +Uses Espressif’s official IDF setup/CI actions — they wrap the official IDF images/tools and are the recommended way to build in GitHub Actions. |
| 261 | +GitHub |
| 262 | ++2 |
| 263 | +GitHub |
| 264 | ++2 |
| 265 | + |
| 266 | +Your preflight script runs the same sequence as you do locally; that parity is the point. |
| 267 | + |
| 268 | +If you later add a self-hosted runner with the Tab5 attached, set SERIAL_PORT in runner env so the boot-loop guard runs automatically and gates the PR. |
| 269 | + |
| 270 | +Why this approach is solid |
| 271 | + |
| 272 | +Exact target selection & build: idf.py set-target esp32p4 is the supported way, and it (re)generates sdkconfig per target. |
| 273 | +Espressif Systems |
| 274 | + |
| 275 | +Unity tests are first-class in IDF; placing tests in test/ with unity.h is canonical. |
| 276 | +Espressif Systems |
| 277 | + |
| 278 | +Boot-loop detection: leveraging known monitor strings (resets, panics, watchdogs) mirrors idf.py monitor output and IDF’s panic handler behavior. |
| 279 | +Espressif Systems |
| 280 | ++1 |
| 281 | + |
| 282 | +Repo-specific layout: Your Tab5 demo uses platforms/tab5 and fetch_repos.py, exactly what we enforce. |
| 283 | +M5Stack Docs |
| 284 | + |
| 285 | +Optional (nice to have, later) |
| 286 | + |
| 287 | +On-target automated tests: Add a tiny pytest file under tests/embedded/ that flashes and asserts a boot banner + single UI tick using pytest-embedded-idf. This follows Espressif’s guidance for automated target testing. |
| 288 | +Espressif Systems |
| 289 | + |
| 290 | +CI matrix for multiple IDF versions (e.g., v5.4.x, latest) using install-esp-idf-action. |
| 291 | +GitHub |
| 292 | + |
| 293 | +Use esp-idf-ci-action if you prefer the Docker-wrapped builder. |
| 294 | +GitHub |
| 295 | + |
| 296 | +What I created for you (summary) |
| 297 | + |
| 298 | +ai/tasks/00_preflight_build_and_bootloop_guard.md — the Codex-visible checklist (first file Codex will read). |
| 299 | + |
| 300 | +scripts/preflight.sh — single entry-point that replicates your local workflow, runs tests, and (optionally) boot-loop check. |
| 301 | + |
| 302 | +scripts/bootloop_guard.py — simple serial log scanner to catch resets/panics. |
| 303 | + |
| 304 | +.github/workflows/idf-preflight.yml — CI to enforce the preflight on every PR/push. |
| 305 | + |
| 306 | +If you want, I can also add a one-line pointer in docs/AI_GUIDELINES.md: |
| 307 | + |
| 308 | +“AI contributors must pass scripts/preflight.sh locally before opening PRs.” |
| 309 | + |
| 310 | +That’s it — from here, Codex has no excuse to ship untested, non-building changes. ✅ |
0 commit comments