Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 94 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ permissions:

jobs:
esp32s3-build:
name: ESP-IDF 5.3.5 / ESP32-S3 ${{ matrix.profile }}
name: ESP-IDF 5.3.5 / ESP32-S3 ${{ matrix.profile }} / baseline
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
profile: [n8r8, tinys3d]
profile: [n8r8, n16r8, tinys3d]
steps:
- name: Check out repository
uses: actions/checkout@v6
Expand All @@ -26,7 +26,72 @@ jobs:
esp_idf_version: v5.3.5
target: esp32s3
path: .
command: bash ci/build-esp32s3.sh ${{ matrix.profile }}
command: bash ci/build-esp32s3.sh ${{ matrix.profile }} baseline

fan-characterization-configured:
name: ESP-IDF 5.3.5 / ESP32-S3 ${{ matrix.board }} / fan-characterization (compile fixture)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# CI COMPILE FIXTURES ONLY. These GPIO numbers exist so hosted CI
# compiles the real fan-characterization backend. They are NOT
# DragonBench reference wiring, NOT JumpJet wiring, and NOT hardware
# recommendations; no fixture has been wired. Each set passes the
# firmware's reserved/conflict guards for its board (TinyS3[D] GPIO38
# RF switch, N16R8 GPIO48 onboard RGB LED, Octal PSRAM 33-37 on N8R8
# and N16R8, strapping, USB, flash, console). n8r8 and tinys3d use
# opposite gate sink levels so both stage polarities compile. n16r8
# matches the DragonBench Rev 0 bench fixture wiring (GPIO6 gate of a
# non-inverting N-channel stage, GPIO7 tach): DragonBench compile/bench
# fixture wiring only, not JumpJet GPIOs, product requirements, or fan
# safety thresholds. PPR stays unknown.
include:
- board: tinys3d
gate_gpio: 4
gate_sink_level: 1
tach_gpio: 5
- board: n8r8
gate_gpio: 6
gate_sink_level: 0
tach_gpio: 7
- board: n16r8
gate_gpio: 6
gate_sink_level: 1
tach_gpio: 7
steps:
- name: Check out repository
uses: actions/checkout@v6
- name: Write compile-only fixture wiring (runner workspace only, never committed)
run: |
cat > sdkconfig.fan-characterization.local <<EOF
# CI COMPILE FIXTURE ONLY: not reference, JumpJet, or recommended wiring.
CONFIG_DB_FAN_PWM_GATE_GPIO=${{ matrix.gate_gpio }}
CONFIG_DB_FAN_GATE_SINK_LEVEL=${{ matrix.gate_sink_level }}
CONFIG_DB_FAN_TACH_GPIO=${{ matrix.tach_gpio }}
EOF
cat sdkconfig.fan-characterization.local
- name: Clean configure and build
uses: espressif/esp-idf-ci-action@v1
with:
esp_idf_version: v5.3.5
target: esp32s3
path: .
command: bash ci/build-esp32s3.sh ${{ matrix.board }} fan-characterization

fan-characterization-refusal:
name: ESP-IDF 5.3.5 / fan-characterization refuses a build without wiring
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v6
- name: Build the experiment profile without its wiring overlay
uses: espressif/esp-idf-ci-action@v1
with:
esp_idf_version: v5.3.5
target: esp32s3
path: .
command: bash ci/build-esp32s3.sh tinys3d fan-characterization --expect-refusal

python-host:
name: Python host contracts
Expand All @@ -43,8 +108,10 @@ jobs:
- name: Run tests
env:
DRAGONBENCH_REQUIRE_NODE: "1"
DRAGONBENCH_REQUIRE_CC: "1"
run: |
node --version
cc --version
python -m unittest discover -s tests -v
- name: Run reset characterization tooling tests
run: python -m unittest tools.tests.test_parsing -v
Expand All @@ -56,21 +123,36 @@ jobs:
set +e
python -m cli.dragonbench run IDLE --duration 0
status=$?
test "$status" -eq 2
python -m cli.dragonbench fan-sweep --pwm-hz 0 --low-pct 0 --high-pct 10 --step-pct 5 --order up --hold-ms 1000 --output sweep.json
status=$?
set -e
test "$status" -eq 2

native-c:
name: Native C state machine
name: Native C state machine (${{ matrix.experiment }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- experiment: baseline
defines: ""
- experiment: fan-characterization
defines: "-DCONFIG_DB_EXPERIMENT_FAN_CHARACTERIZATION=1"
steps:
- name: Check out repository
uses: actions/checkout@v6
- name: Compile with warnings and sanitizers
run: >-
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror
-fsanitize=address,undefined -fno-omit-frame-pointer
-Ifirmware/common/include
firmware/common/db_run.c firmware/common/db_network.c tests/native/test_db_run.c
-o test_db_run
- name: Execute state-machine test
run: ./test_db_run
run: |
for test in test_db_run test_db_fan; do
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer ${{ matrix.defines }} \
-Ifirmware/common/include \
firmware/common/db_run.c firmware/common/db_network.c firmware/common/db_fan.c \
tests/native/$test.c -o $test
done
- name: Execute state-machine and fan-characterization tests
run: |
./test_db_run
./test_db_fan
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@ __pycache__/
.venv/
.pytest_cache/
artifacts/
sdkconfig.fan-characterization.local
test_db_fan
test_db_fan.exe
idf-build.log
53 changes: 45 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,28 @@ rail stability, and reset/brownout behavior.

It is not JumpJet or DragonBreath firmware, a performance benchmark, a generic
HAL, a device-control application, or an authority for product safety limits.
The firmware contains no heater or fan GPIOs and exposes no actuator API.

Version 0.1.0 builds for two ESP32-S3 board profiles, both with 8 MB flash and
8 MB PSRAM: the N8R8 module (Octal SPI PSRAM, CH343P USB-UART bridge; the
default) and the Unexpected Maker TinyS3[D] (Quad SPI PSRAM, native USB
Serial/JTAG, onboard/U.FL RF switch). Browser and CLI clients use the same
versioned HTTP/JSON API. DUT events identify workload boundaries; all
voltage/current evidence remains owned by external instruments.
Every image is one board profile plus one
[experiment profile](docs/EXPERIMENT_PROFILES.md). The default `baseline`
profile contains no heater or fan GPIOs and exposes no actuator API. The
[`fan-characterization` profile](docs/FAN_CHARACTERIZATION.md) adds a single
stimulus workload, `FAN_PWM_HOLD`, that drives only the gate of an external
open-drain stage and counts tach edges. That is characterization equipment,
not product fan control: no profile has a heater path, a closed loop,
thresholds, or fan-safety policy, and `heater_capability` and
`fan_control_capability` are `false` in every image. `/api/v1/device` reports
the board profile, experiment profile, build revision, device identity, and
where each measured quantity comes from.

Version 0.1.0 builds for three ESP32-S3 board profiles, all with 8 MB PSRAM:
the N8R8 module (8 MB flash, Octal SPI PSRAM, CH343P USB-UART bridge; the
default), the N16R8 module on a Lonely Binary carrier (16 MB flash, Octal SPI
PSRAM, onboard RGB LED on GPIO48), and the Unexpected Maker TinyS3[D] (8 MB
flash, Quad SPI PSRAM, native USB Serial/JTAG, onboard/U.FL RF switch).
Each board's onboard RGB LED is DragonBench's status light: green when ready
and idle, off at boot and throughout every run, and never available to
fixture wiring. Browser and CLI clients use the same versioned HTTP/JSON API.
DUT events identify workload boundaries; all voltage/current evidence remains
owned by external instruments.

## Quick start

Expand All @@ -42,6 +56,15 @@ idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.tinys3d" set
idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.tinys3d" build
```

For the N16R8 module, layer its overlay the same way. It keeps the shared
partition layout, which ends below 8 MB, so the upper half of its 16 MB flash
is unused:

```text
idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.n16r8" set-target esp32s3
idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.n16r8" build
```

Delete a generated `sdkconfig` before switching profiles; existing values take
precedence over the defaults files. The TinyS3[D] has no auto-reset circuit on
its native USB port: hold BOOT, tap RESET, release BOOT, then flash with
Expand Down Expand Up @@ -83,6 +106,7 @@ python -m cli.dragonbench traffic-peer --port 5001 --mode echo --duration 60
```

See [bench workflow](docs/BENCH_WORKFLOW.md), [target profile](docs/TARGET_ESP32S3.md),
[experiment profiles](docs/EXPERIMENT_PROFILES.md), [fan characterization](docs/FAN_CHARACTERIZATION.md),
and [protocol contract](protocol/openapi.yaml).

## Validation status
Expand All @@ -109,6 +133,19 @@ yet been exercised on hardware, and workload execution and electrical
characterization remain unvalidated there too. See
[TinyS3[D] profile](docs/TARGET_TINYS3D.md).

On one N16R8 board, the baseline image's flash, boot, the 8 MB Octal PSRAM
test, the status LED, flashing through both USB-C connectors, the direct access
point, mDNS and the read-only API have been validated. Station mode, workload
execution and electrical characterization remain unvalidated there. See
[N16R8 profile](docs/TARGET_N16R8.md).

The `fan-characterization` experiment profile builds with ESP-IDF 5.3.5 once
its wiring is stated, and refuses to build without it. Its stimulus lifecycle
and tach arithmetic pass host tests. Its fixture wiring is provisional. On the
N16R8, with that wiring and nothing attached, the image boots and holds the
gate pin at the expected static levels for 0% and 100% sink duty. The fixture,
fan, tach, PWM waveform and PPR remain unvalidated on hardware.

## Scope rule

A workload belongs here only when it answers a concrete electrical
Expand Down
108 changes: 101 additions & 7 deletions ci/build-esp32s3.sh
Original file line number Diff line number Diff line change
@@ -1,25 +1,92 @@
#!/usr/bin/env bash
set -euo pipefail

profile="${1:-n8r8}"
case "$profile" in
# Usage: ci/build-esp32s3.sh <board> [experiment] [--expect-refusal]
# board: n8r8 | n16r8 | tinys3d
# experiment: baseline (default) | fan-characterization
#
# An image is one board profile plus one experiment profile. Each experiment
# layers sdkconfig.defaults.<experiment>; one that needs fixture wiring also
# layers the untracked sdkconfig.<experiment>.local. --expect-refusal builds
# the experiment WITHOUT its local overlay and passes only if the build stops
# naming every missing wiring setting.
board="${1:-n8r8}"
experiment="${2:-baseline}"
expect_refusal="${3:-}"
case "$board" in
n8r8)
defaults="sdkconfig.defaults"
expect=("CONFIG_SPIRAM_MODE_OCT 1" 'CONFIG_DB_TARGET_NAME "esp32s3-n8r8"' "CONFIG_DB_RF_SWITCH_GPIO -1")
expect=("CONFIG_SPIRAM_MODE_OCT 1" "CONFIG_ESPTOOLPY_FLASHSIZE_8MB 1" 'CONFIG_DB_TARGET_NAME "esp32s3-n8r8"'
"CONFIG_DB_RF_SWITCH_GPIO -1" "CONFIG_DB_STATUS_RGB_GPIO 48" "CONFIG_DB_STATUS_RGB_POWER_GPIO -1")
;;
n16r8)
defaults="sdkconfig.defaults;sdkconfig.defaults.n16r8"
expect=("CONFIG_SPIRAM_MODE_OCT 1" "CONFIG_ESPTOOLPY_FLASHSIZE_16MB 1" 'CONFIG_DB_TARGET_NAME "esp32s3-n16r8"'
"CONFIG_DB_RF_SWITCH_GPIO -1" "CONFIG_DB_STATUS_RGB_GPIO 48" "CONFIG_DB_STATUS_RGB_POWER_GPIO -1")
;;
tinys3d)
defaults="sdkconfig.defaults;sdkconfig.defaults.tinys3d"
expect=("CONFIG_SPIRAM_MODE_QUAD 1" 'CONFIG_DB_TARGET_NAME "esp32s3-tinys3d"' "CONFIG_DB_RF_SWITCH_GPIO 38")
expect=("CONFIG_SPIRAM_MODE_QUAD 1" "CONFIG_ESPTOOLPY_FLASHSIZE_8MB 1" 'CONFIG_DB_TARGET_NAME "esp32s3-tinys3d"'
"CONFIG_DB_RF_SWITCH_GPIO 38" "CONFIG_DB_STATUS_RGB_GPIO 18" "CONFIG_DB_STATUS_RGB_POWER_GPIO 17"
"CONFIG_DB_STATUS_RGB_POWER_ACTIVE_LEVEL 1")
;;
*)
echo "unknown board profile: $board (expected n8r8, n16r8, or tinys3d)" >&2
exit 2
;;
esac

# Non-baseline experiments, and what must be absent from every other image.
all_experiments=(fan-characterization)
declare -A experiment_symbol=([fan-characterization]="FAN_CHARACTERIZATION")
declare -A experiment_code=([fan-characterization]="ledc_|pcnt_|fan_characterization_")
declare -A experiment_entry=([fan-characterization]="fan_characterization_hold")
declare -A experiment_wiring=([fan-characterization]="CONFIG_DB_FAN_PWM_GATE_GPIO CONFIG_DB_FAN_GATE_SINK_LEVEL CONFIG_DB_FAN_TACH_GPIO")

case "$experiment" in
baseline)
expect+=("CONFIG_DB_EXPERIMENT_BASELINE 1")
;;
fan-characterization)
defaults="$defaults;sdkconfig.defaults.$experiment"
expect+=("CONFIG_DB_EXPERIMENT_${experiment_symbol[$experiment]} 1")
if [[ -z "$expect_refusal" ]]; then
if [[ ! -f "sdkconfig.$experiment.local" ]]; then
echo "$experiment needs sdkconfig.$experiment.local stating the wired fixture (docs/FAN_CHARACTERIZATION.md)" >&2
exit 2
fi
defaults="$defaults;sdkconfig.$experiment.local"
fi
;;
*)
echo "unknown board profile: $profile (expected n8r8 or tinys3d)" >&2
echo "unknown experiment profile: $experiment (expected baseline or ${all_experiments[*]})" >&2
exit 2
;;
esac
if [[ -n "$expect_refusal" && ( "$expect_refusal" != --expect-refusal || "$experiment" == baseline ) ]]; then
echo "--expect-refusal applies only to an experiment that needs fixture wiring" >&2
exit 2
fi

rm -f sdkconfig
idf.py -D SDKCONFIG_DEFAULTS="$defaults" fullclean
idf.py -D SDKCONFIG_DEFAULTS="$defaults" set-target esp32s3

if [[ -n "$expect_refusal" ]]; then
if idf.py -D SDKCONFIG_DEFAULTS="$defaults" build > idf-build.log 2>&1; then
echo "unconfigured $experiment build succeeded; it must refuse to build" >&2
exit 1
fi
for setting in ${experiment_wiring[$experiment]}; do
if ! grep -q "#error \"$experiment: set $setting" idf-build.log; then
echo "unconfigured $experiment build did not name $setting" >&2
exit 1
fi
done
echo "$board/$experiment without wiring refused as expected"
exit 0
fi

idf.py -D SDKCONFIG_DEFAULTS="$defaults" build 2>&1 | tee idf-build.log

if grep -Ei 'warning:|error:' idf-build.log; then
Expand All @@ -29,8 +96,35 @@ fi

for line in "${expect[@]}"; do
if ! grep -qF "#define $line" build/config/sdkconfig.h; then
echo "profile $profile did not produce '#define $line'" >&2
echo "$board/$experiment did not produce '#define $line'" >&2
exit 1
fi
done

nm_tool="${NM:-xtensa-esp32s3-elf-nm}"
# Fail rather than pass vacuously if the symbol table cannot be read.
symbols="$("$nm_tool" build/dragonbench.elf)"
if ! grep -q ' app_main$' <<< "$symbols"; then
echo "could not read symbols from build/dragonbench.elf with $nm_tool" >&2
exit 1
fi
for other in "${all_experiments[@]}"; do
code="$(grep -E " (${experiment_code[$other]})" <<< "$symbols" || true)"
if [[ "$other" == "$experiment" ]]; then
if ! grep -q " ${experiment_entry[$other]}$" <<< "$code"; then
echo "$board/$experiment does not link ${experiment_entry[$other]}" >&2
exit 1
fi
continue
fi
if grep -q "CONFIG_DB_EXPERIMENT_${experiment_symbol[$other]}" build/config/sdkconfig.h; then
echo "$board/$experiment also selected $other" >&2
exit 1
fi
if [[ -n "$code" ]]; then
echo "$board/$experiment links $other code:" >&2
echo "$code" >&2
exit 1
fi
done
echo "profile $profile verified"
echo "$board/$experiment verified"
Loading
Loading