Files
thermostat-hw/sdkconfig.defaults
T
mattandClaude 3aabcac90a Add thermostat control for heat and fan with cycle protection
Turns the weather dashboard into an actual thermostat. Relays are not
fitted yet -- the GPIOs are driven and can be metered at the UART1-OUT
plug (idle 3.3V, energized 0V, active-low module assumed).

Control loop (main/control/thermostat.c) is pure logic: no ESP-IDF, no
network, no clock of its own. The caller passes a temperature and a
millisecond timestamp, which is what lets the simulator and the test
harness run the identical decision code.

Equipment protections, all verified by tools/thermostat_test.c:

  deadband      1.0F    stops the burner chattering at the setpoint
  minimum on    3 min   a shorter call is all ignition wear, no heat
  minimum off   5 min   back-to-back ignitions are what kill igniters
  cycles/hour   5       standard gas default; catches a flapping sensor
  boot lockout  60 s    a boot loop must not machine-gun the gas valve

That test drives the state machine with synthetic time, so two hours of
furnace behaviour runs in milliseconds. It exits non-zero on failure and
is worth re-running after any edit to thermostat.c -- this switches a gas
valve.

Two deliberate decisions:

  - G is never energized during a heat call. The furnace runs its own
    blower with a fan-on delay that waits for the heat exchanger and a
    fan-off delay that scavenges residual heat; driving G ourselves would
    blow cold air at people and throw that heat away. G is manual
    circulation only.
  - The control loop starts before every early return in app_main. Those
    returns are all "no WiFi" or "no HA token", and none of them is a
    reason to leave a house unheated.

Relay pins are IO20 heat / IO19 fan on the UART1-OUT plug, measured on
this board since Elecrow documents the GPIO numbers but not the connector
pin order. Heat is on IO20 because it is USB D+, the line carrying the
hardware pull-up -- with an active-low module it fails toward "no heat"
rather than "heat stuck on" if the USB PHY is ever re-enabled. Both pins
were confirmed free of the onboard mic before use.
CONFIG_USJ_ENABLE_USB_SERIAL_JTAG and CONFIG_ESP_PHY_ENABLE_USB are now
off to release them; the console is on UART0 via the CH340 regardless.

UI: the indoor card gains a status dot, mode and fan buttons, and a
setpoint column with finger-sized up/down targets. The reading grid drops
from six cells to four to make room -- wind was the one to cut, since the
station anemometer has been dead since 2026-07-27 and that cell has never
shown a number. Wind now rides on the outdoor cell's sub-line and will
reappear on its own if the sensor is fixed. Sunrise and sunset share a
cell, showing whichever comes next.

Known weakness, documented in README and top of TODO: indoor temperature
comes from Home Assistant over the network, so an HA outage ages the
reading until the 15-minute failsafe forces heat off. A local I2C sensor
removes the failure mode entirely and is the next thing to add. The
setpoint also resets to 68F on reboot, because persisting it means an NVS
write and a flash write while the panel scans faults the bounce-buffer
ISR.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 20:09:36 -07:00

101 lines
3.9 KiB
Plaintext

# Elecrow CrowPanel 7.0" Advance HMI -- ESP32-S3-WROOM-1-N16R8
# 16MB flash, 8MB octal PSRAM, 800x480 RGB panel.
CONFIG_IDF_TARGET="esp32s3"
# --- Flash / partitions ---
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_ESPTOOLPY_FLASHMODE_QIO=y
CONFIG_ESPTOOLPY_FLASHFREQ_80M=y
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
# --- CPU ---
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
CONFIG_FREERTOS_HZ=1000
# --- PSRAM ---
# The R8 suffix is 8MB OCTAL PSRAM. QSPI mode will not sustain an 800x480
# RGB framebuffer; octal at 80MHz is required for a stable panel.
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_OCT=y
# 80MHz. The vendor's Arduino build uses 120MHz, which would give the RGB
# panel 50% more bandwidth -- but on ESP-IDF 5.3 selecting 120MHz PSRAM raises
# the shared MSPI core clock and forces flash to 120MHz too. That needs flash
# High Performance Mode plus a DC-aware bootloader, and getting it wrong
# leaves the board unbootable. Not worth it; the bounce buffer covers the gap.
CONFIG_SPIRAM_SPEED_80M=y
CONFIG_SPIRAM_USE_MALLOC=y
# Do NOT enable SPIRAM_FETCH_INSTRUCTIONS / SPIRAM_RODATA here. Espressif
# documents them as incompatible with an RGB panel framebuffer in PSRAM: the
# LCD DMA and the instruction fetch path contend for the same bus, which
# blanks the panel and stalls TLS handshakes.
# --- Cache: reduces RGB panel tearing/drift ---
CONFIG_ESP32S3_DATA_CACHE_64KB=y
CONFIG_ESP32S3_DATA_CACHE_LINE_32B=y
# --- LCD ---
CONFIG_LCD_RGB_ISR_IRAM_SAFE=y
# Re-align the scanout to vsync if it ever does slip.
CONFIG_LCD_RGB_RESTART_IN_VSYNC=y
# The bounce-buffer ISR faults if the cache is disabled by a flash write while
# the panel is running. app_main finishes WiFi bring-up before starting the
# panel; this stops the WiFi stack writing to flash afterwards too.
CONFIG_ESP_WIFI_NVS_ENABLED=n
# --- WiFi buffer trim ---
# Frees ~25KB of internal RAM so the LVGL draw buffers fit there rather than
# in PSRAM. Keeping them out of PSRAM removes a third round trip over the same
# bus the LCD is streaming from, which is what causes the refill artefacts.
# This board only polls a small JSON endpoint, so the smaller pools are ample.
CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=4
CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=16
CONFIG_ESP_WIFI_TX_BUFFER_TYPE=1
CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM=16
CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=n
CONFIG_ESP_WIFI_AMPDU_RX_ENABLED=n
# --- Console ---
# Console reaches us over the CH340 UART bridge. The USB-Serial-JTAG
# peripheral is left off: it is hardwired to GPIO19/20 on the ESP32-S3, which
# the Basic CrowPanel uses for touch I2C. The Advance uses 15/16 so this is
# not strictly required here, but nothing needs it and it keeps those pins free.
CONFIG_ESP_CONSOLE_SECONDARY_NONE=y
# --- TLS ---
# The LVGL draw buffers take ~62KB of internal RAM, which leaves too little
# for an mbedTLS session (it fails with MBEDTLS_ERR_SSL_ALLOC_FAILED, -0x7F00).
# Push TLS allocations to PSRAM and shrink the outgoing content buffer -- the
# only request this makes is a short GET.
CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y
CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096
# --- LVGL 9 ---
CONFIG_LV_COLOR_DEPTH_16=y
CONFIG_LV_USE_LOG=y
CONFIG_LV_LOG_LEVEL_WARN=y
# Must stay identical to the LV_FONT_MONTSERRAT_* set in sim/lv_conf.h. A font
# enabled in one build but not the other makes the simulator lie about layout:
# text silently falls back to a different size and every metric shifts.
CONFIG_LV_FONT_MONTSERRAT_12=y
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_16=y
CONFIG_LV_FONT_MONTSERRAT_20=y
CONFIG_LV_FONT_MONTSERRAT_24=y
CONFIG_LV_FONT_MONTSERRAT_28=y
CONFIG_LV_FONT_MONTSERRAT_32=y
CONFIG_LV_FONT_MONTSERRAT_36=y
CONFIG_LV_FONT_MONTSERRAT_40=y
CONFIG_LV_FONT_MONTSERRAT_48=y
# --- Main task needs headroom for driver init ---
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
CONFIG_LV_USE_TINY_TTF=y
# CONFIG_USJ_ENABLE_USB_SERIAL_JTAG is not set
# CONFIG_ESP_PHY_ENABLE_USB is not set