Implement rotary thermostat firmware from PRD
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>
This commit is contained in:
1 parent
393592e327
commit
d2e4c974dd
35 files changed
+4685
No files matched your search
@@ -0,0 +1,96 @@
|
||||
#include "cst826.h"
|
||||
|
||||
#include "display.h"
|
||||
#include "esp_log.h"
|
||||
#include "i2c_bus.h"
|
||||
|
||||
static const char *TAG = "cst826";
|
||||
|
||||
#define REG_NUM_TOUCHES 0x02
|
||||
#define REG_TOUCH_DATA 0x03
|
||||
#define REG_CHIP_TYPE 0xAA
|
||||
|
||||
static i2c_master_dev_handle_t s_dev;
|
||||
static bool s_ready;
|
||||
|
||||
static esp_err_t read_reg(uint8_t reg, uint8_t *buf, size_t len)
|
||||
{
|
||||
return i2c_master_transmit_receive(s_dev, ®, 1, buf, len, 100);
|
||||
}
|
||||
|
||||
esp_err_t cst826_init(void)
|
||||
{
|
||||
if (s_ready) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t err = i2c_bus_add_device(CST826_I2C_ADDR, &s_dev);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "add device 0x%02X failed: %s",
|
||||
CST826_I2C_ADDR, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
uint8_t id = 0;
|
||||
err = read_reg(REG_CHIP_TYPE, &id, 1);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "no response at 0x%02X: %s",
|
||||
CST826_I2C_ADDR, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
if (id != CST826_CHIP_ID) {
|
||||
/* Not fatal — the thermostat is fully usable from the encoder alone,
|
||||
* and refusing to boot over a touch panel would be a poor trade for
|
||||
* something that controls heat. */
|
||||
ESP_LOGW(TAG, "unexpected chip id 0x%02X (expected 0x%02X) — "
|
||||
"continuing with touch disabled", id, CST826_CHIP_ID);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
s_ready = true;
|
||||
ESP_LOGI(TAG, "ready at 0x%02X, chip id 0x%02X", CST826_I2C_ADDR, id);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool cst826_is_ready(void)
|
||||
{
|
||||
return s_ready;
|
||||
}
|
||||
|
||||
void cst826_read(cst826_touch_t *out)
|
||||
{
|
||||
if (!out) {
|
||||
return;
|
||||
}
|
||||
out->pressed = false;
|
||||
out->x = 0;
|
||||
out->y = 0;
|
||||
|
||||
if (!s_ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t n = 0;
|
||||
if (read_reg(REG_NUM_TOUCHES, &n, 1) != ESP_OK || n == 0 || n > 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t d[6];
|
||||
if (read_reg(REG_TOUCH_DATA, d, sizeof(d)) != ESP_OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t x = (uint16_t)((d[0] & 0x0F) << 8 | d[1]);
|
||||
uint16_t y = (uint16_t)((d[2] & 0x0F) << 8 | d[3]);
|
||||
|
||||
/* A garbage read past the panel edge means a bad transfer, not a touch at
|
||||
* the corner. Drop it rather than sending LVGL a bogus press. */
|
||||
if (x >= DISPLAY_H_RES || y >= DISPLAY_V_RES) {
|
||||
return;
|
||||
}
|
||||
|
||||
out->x = x;
|
||||
out->y = y;
|
||||
out->pressed = true;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* CST826 capacitive touch controller @ I2C 0x15.
|
||||
*
|
||||
* Note this is CST826, not the CST816 the PRD names. Same family, but the chip
|
||||
* ID register reads 0x11 rather than 0xB4, so a CST816 driver's probe rejects
|
||||
* it and the board looks broken. Register map below is taken from the
|
||||
* Adafruit_CST8XX library bundled with Elecrow's example for this board.
|
||||
*
|
||||
* Protocol: read register 0x02 for the touch count, then a burst from 0x03 of
|
||||
* 6 bytes per touch:
|
||||
*
|
||||
* byte 0: [7:6] event, [3:0] X high nibble
|
||||
* byte 1: X low byte
|
||||
* byte 2: [7:4] touch id, [3:0] Y high nibble
|
||||
* byte 3: Y low byte
|
||||
* bytes 4,5: unused
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#define CST826_I2C_ADDR 0x15
|
||||
#define CST826_CHIP_ID 0x11
|
||||
|
||||
typedef struct {
|
||||
uint16_t x;
|
||||
uint16_t y;
|
||||
bool pressed;
|
||||
} cst826_touch_t;
|
||||
|
||||
/*
|
||||
* Initialise and verify the chip ID.
|
||||
*
|
||||
* Requires i2c_bus_init() and the reset pulse that display.c's power-up
|
||||
* sequence performs on expander P0.
|
||||
*/
|
||||
esp_err_t cst826_init(void);
|
||||
|
||||
bool cst826_is_ready(void);
|
||||
|
||||
/* Latest touch state. Never fails hard — an I2C error reports "not pressed"
|
||||
* so a flaky touch controller cannot wedge the UI task. */
|
||||
void cst826_read(cst826_touch_t *out);
|
||||
Reference in new issue
Block a user