Nine ESP32-S3 boards arrive with nothing on them. This sheet takes you from that to eight nodes regulating one shared bath and a gauge drawing the curve — without guessing. Read the build sheet first; this is the software half of the same job.
| Qty | Item | Notes |
|---|---|---|
| 9 | ESP32-S3 N16R8 (DevKitC-1 layout) | 8 nodes + 1 gauge; the gauge is deliberately not one of the eight |
| 1 | USB-C cable that carries data | a charge-only cable enumerates nothing; this is a common time-sink |
| — | jedas-side tooling | esptool, mpremote, pyserial, matplotlib |
pip install --user esptool mpremote pyserial matplotlib
Download page: https://micropython.org/download/ESP32_GENERIC_S3/
Take the "Support for Octal-SPIRAM" (SPIRAM_OCT) variant. The plain generic S3 build auto-detects quad SPIRAM but does not talk to octal PSRAM; the N16R8 boards have octal PSRAM and need the octal variant. Grabbing the wrong build is the single most common failure on this board — the board will flash and boot, then behave oddly or short on memory.
Find the serial device. This board's USB-serial bridge enumerates as CDC-ACM, so
look for /dev/ttyACM*, not ttyUSB*:
ls /dev/ttyACM* /dev/ttyUSB*
Permissions, the durable way — add yourself to dialout, then log out and
back in (a one-off chmod on the device is only a stopgap):
sudo usermod -aG dialout $USER
Erase, then write the octal build. The offset is 0 on the S3 — not
0x1000 as on the classic ESP32:
esptool --chip esp32s3 -p /dev/ttyACM0 erase-flash
esptool --chip esp32s3 -p /dev/ttyACM0 -b 921600 \
write-flash -z 0 ESP32_GENERIC_S3-SPIRAM_OCT-<version>.bin
If the board will not enter the bootloader on its own: hold BOOT, tap RESET, release BOOT, then flash. These boards have two USB-C ports (one bridged UART, one native USB). If one port will not take the flash, try the other with the manual BOOT/RESET entry above — try, and if it does not go, try the other port.
Verify you have a live MicroPython:
mpremote connect /dev/ttyACM0
That should drop you at a REPL. Ctrl-D soft-resets the board;
Ctrl-X exits mpremote.
Every board runs one file, copied in as main.py. A node:
mpremote connect /dev/ttyACM0 fs cp firmware/node_lab1.py :main.py
The gauge:
mpremote connect /dev/ttyACM0 fs cp firmware/gauge.py :main.py
Every node gets the identical node_lab1.py — there is no node id, no address, nothing to configure per board. Label the boards 1–8 with tape for your own bookkeeping; the firmware neither knows nor needs to know which one it is, because noding tells no one anything. The one gauge board gets gauge.py instead.
This mirrors build sheet §5 — do it once, on one node, before you commit the whole batch.
1.0.With the box closed and all nine boards powered, log the gauge from jedas. The logger
auto-detects the port; --list shows candidates and --out names
the file:
python3 tools/gauge_logger.py --out run1.csv
While it runs, type an event note and press Enter to mark it — e.g.
killed node 3. The marker lands in the CSV with a timestamp and is drawn as
a labelled vertical line on the plot. This is what makes the perturbation runs in
build sheet §6 legible — the curve stops
being a squiggle and starts being an argument.
Plot any time — it is safe to run against a log still being written, so check the shape at minute 5 rather than waiting out the whole run:
python3 tools/plot_settling.py run1.csv
Tools: gauge_logger.py · plot_settling.py
None of these are bugs — do not let a class mistake the physics for a fault.
HEATER = "busy": the CPU itself, ~0.2–0.3 W per
node, no extra hardware. Gentle authority.SETPOINT
to a few degrees above the actual room ambient, or build the optional MOSFET + 10 Ω
chain from build sheet §2 and set
HEATER = "pwm". The droop law demonstrates identically either way.The S3 pin law, as established in the S3 migration. Lab 1 senses on a 1-Wire pin (GPIO 4) and drives its optional heater on GPIO 16; labs 2–3 read an analog rail on ADC1 (GPIO 5). Carried here so you flash against the right sheet:
| Signal | Pin | Where |
|---|---|---|
| DS18B20 data (sense) | GPIO 4 | lab 1 |
| Load PWM | GPIO 16 | lab 1 (only if HEATER = "pwm") |
| ADC1 sense | GPIO 5 | labs 2–3 |
Reserved on the N16R8 — never wire to these: GPIO 26–32 are SPI flash and GPIO 33–37 are octal PSRAM; touching them crashes the cache. Safe GPIOs are 1–18, 21, 38–42, 47.