ESP-IDF v5.3.2 firmware for the CrowPanel 2.1" rotary display. Builds clean;
nothing has been run on hardware yet.
Hardware verified against Elecrow's own source for this board and
cross-checked against their ESPHome config, per PRD 8.1. Three corrections to
the PRD's pinout, all documented in docs/HARDWARE.md:
- touch is CST826 at 0x15, not CST816 (chip ID reads 0x11, not 0xB4)
- the ST7701S needs a 3-wire SPI bus on GPIO 16/2/1 that the PRD omitted;
without it the init sequence cannot be sent and the panel stays black
- the backlight is GPIO6
Two further departures from the PRD, both forced by facts on the ground:
- Transport is MQTT, not HTTP. zigbee2mqtt has no REST endpoint for live
sensor state. Subscribing to the broker also gets retained topics, so
every room's last reading arrives on connect rather than after a poll.
- The furnace is 2-wire R/W with no C, not 3-wire. The board needs its own
5V supply. The thermostat's 5 CPH setting is enforced as a minimum
12-minute period between heat-call starts, which a minimum off-time alone
does not achieve.
All of PRD section 5 is implemented in the relay layer itself: fail-open,
watchdog-gated output, max on-time, min off-time, cycle-rate limit, startup
lockout, and the stale-data fallback ladder down to open-loop freeze
protection. The one gap that cannot be closed in software — the PCF8574 not
resetting when the ESP32 does — is documented rather than papered over.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
108 lines
3.9 KiB
C
108 lines
3.9 KiB
C
/*
|
|
* control — setpoint, hysteresis, room selection, and the stale-data ladder.
|
|
*
|
|
* Runs on the ESP32, not in Home Assistant. The house keeps heating when the
|
|
* broker, zigbee2mqtt or HA is down; it degrades through the documented
|
|
* fallback ladder rather than simply stopping.
|
|
*
|
|
* All temperatures in this module are degrees Celsius. Fahrenheit exists only
|
|
* in the UI layer.
|
|
*/
|
|
#pragma once
|
|
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
|
|
#include "esp_err.h"
|
|
|
|
/* -------------------------------------------------------------- tuning -- */
|
|
|
|
/* Evaluate on a fixed cadence, not on every incoming reading (PRD 4.3). */
|
|
#define CONTROL_INTERVAL_S 10
|
|
|
|
/* Hysteresis. Call for heat below setpoint-deadband, stop at setpoint+deadband. */
|
|
#define CONTROL_DEADBAND_C 0.5f
|
|
|
|
#define CONTROL_SETPOINT_MIN_C 5.0f
|
|
#define CONTROL_SETPOINT_MAX_C 30.0f
|
|
#define CONTROL_SETPOINT_DEFAULT_C 20.0f /* exactly 68F */
|
|
|
|
/* A reading older than this no longer governs anything (PRD 5.5, N=20). */
|
|
#define CONTROL_STALE_AFTER_S (20 * 60)
|
|
|
|
/*
|
|
* Conservative setpoint used once we are running on stale or absent data.
|
|
* Low enough that a wrong reading cannot cook the house, high enough to keep
|
|
* pipes safe.
|
|
*/
|
|
#define CONTROL_FALLBACK_SETPOINT_C 16.0f
|
|
|
|
/*
|
|
* Last-resort freeze protection, used only when there is no reading at all —
|
|
* not stale, not old, none since boot.
|
|
*
|
|
* With no temperature there is no closed loop to run, so this is an open-loop
|
|
* duty cycle: heat for ON, then wait at least OFF. PRD 5.5 requires that
|
|
* stale data must never hold the relay closed and must never silently mean no
|
|
* heat either; with zero data those are the only two failure modes available,
|
|
* and a low duty cycle sits between them. Every relay interlock still applies
|
|
* on top, so this cannot short-cycle the furnace.
|
|
*/
|
|
#define CONTROL_FREEZE_DUTY_ON_S (6 * 60)
|
|
#define CONTROL_FREEZE_DUTY_OFF_S (24 * 60)
|
|
|
|
/* --------------------------------------------------------------- modes -- */
|
|
|
|
typedef enum {
|
|
ROOM_MODE_MANUAL = 0, /* encoder picks the governing room */
|
|
ROOM_MODE_SCHEDULE, /* time-of-day rules */
|
|
ROOM_MODE_FORMULA, /* min across rooms — coldest room wins */
|
|
ROOM_MODE_COUNT,
|
|
} room_mode_t;
|
|
|
|
/*
|
|
* How the governing temperature is currently being derived. This is the
|
|
* fallback ladder of PRD 5.5, in order of preference, and it is what the UI
|
|
* renders as a fault.
|
|
*/
|
|
typedef enum {
|
|
CONTROL_SRC_SELECTED = 0, /* the chosen room, fresh */
|
|
CONTROL_SRC_OTHER_ROOM, /* chosen room stale, another is fresh */
|
|
CONTROL_SRC_STALE, /* nothing fresh; running aged data, clamped */
|
|
CONTROL_SRC_NONE, /* no reading at all; freeze-protection duty */
|
|
} control_source_t;
|
|
|
|
typedef struct {
|
|
float setpoint_c; /* what the user asked for */
|
|
float effective_setpoint_c; /* after any fallback clamp */
|
|
float governing_temp_c; /* NaN when source is CONTROL_SRC_NONE */
|
|
int governing_room; /* index into the room table, -1 if none */
|
|
room_mode_t mode;
|
|
control_source_t source;
|
|
bool calling_for_heat; /* what control asked the relay for */
|
|
bool schedule_clock_valid;
|
|
} control_state_t;
|
|
|
|
/* Start the control task. Requires relay_init() and zigbee_client_init(). */
|
|
esp_err_t control_init(void);
|
|
|
|
void control_get_state(control_state_t *out);
|
|
|
|
/* Setpoint, clamped to bounds and persisted to NVS. */
|
|
void control_set_setpoint(float celsius);
|
|
void control_adjust_setpoint(float delta_c);
|
|
float control_get_setpoint(void);
|
|
|
|
void control_set_mode(room_mode_t mode);
|
|
room_mode_t control_get_mode(void);
|
|
|
|
/*
|
|
* Manual room selection. Also switches to ROOM_MODE_MANUAL — PRD 4.2 requires
|
|
* manual override to be reachable regardless of the active mode.
|
|
*/
|
|
void control_select_room(int room_index);
|
|
int control_get_selected_room(void);
|
|
|
|
const char *control_source_str(control_source_t src);
|
|
const char *control_mode_str(room_mode_t mode);
|