Keep the HTTP session alive between weather polls (TODO #1). Re-creating the client each time forced a fresh TLS handshake inside the window where the UI is frozen. Measured on hardware: the fetch went from ~1.3s to ~80ms, so the pause is no longer long enough to matter once thermostat controls exist. The timeout drops from 30s to 10s once a session is warm, since only a cold handshake needs the longer budget. Any transport error or non-200 tears the session down so a half-open connection cannot poison every later poll. The response buffer is now reset per request rather than on HTTP_EVENT_ON_CONNECTED -- a kept-alive connection does not re-fire that event, so the buffer would have accumulated across polls. Indicate stale readings (TODO #2). After two consecutive failures the temperature dims and the status line shows the age. Age comes from tick counts, not weather_data_t.last_update: there is no SNTP client here, so time(NULL) starts at the epoch and any age derived from it is meaningless. Gate the touch readout behind UI_DEBUG_TOUCH, default off (TODO #3). Kept the note about LVGL not bubbling events to parents -- it documents a real trap. Delete main/utils (TODO #4); it had no callers outside itself. Also refreshed project_structure.txt, which was additionally missing i2c_bus and backlight. Lay out two 4MB OTA slots (TODO #5) while no devices are fielded, and document in the README that an OTA download cannot run while the panel is scanning -- the flash write faults the bounce-buffer ISR. Log the truncation warning once per response instead of once per chunk, and record the weather task's stack high-water mark (TODO #6): ~2400 bytes spare of 6144. Co-Authored-By: Claude <noreply@anthropic.com>
190 lines
6.6 KiB
C
190 lines
6.6 KiB
C
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include "esp_log.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
|
|
#include "ambient_weather.h"
|
|
#include "app_config.h"
|
|
#include "backlight.h"
|
|
#include "display_rgb.h"
|
|
#include "i2c_bus.h"
|
|
#include "lvgl_port.h"
|
|
#include "touch_gt911.h"
|
|
#include "ui_manager.h"
|
|
#include "wifi_manager.h"
|
|
|
|
static const char *TAG = "main";
|
|
|
|
/*
|
|
* 6144 bytes. Measured headroom after a fetch including the TLS handshake --
|
|
* the deepest this task gets -- is ~2400 bytes, so roughly 3.7KB is in use.
|
|
* mbedTLS keeps its heap in PSRAM (CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC), which
|
|
* is why the handshake fits at all. Re-check the high-water-mark log if more
|
|
* work lands in this task.
|
|
*/
|
|
#define WEATHER_TASK_STACK 6144
|
|
#define WEATHER_TASK_PRIO 3
|
|
#define WEATHER_FIRST_DELAY_MS 3000
|
|
|
|
/* Consecutive failed polls before the displayed reading is treated as stale. */
|
|
#define WEATHER_STALE_AFTER_POLLS 2
|
|
|
|
static void weather_update_task(void *arg)
|
|
{
|
|
weather_data_t weather;
|
|
char line[96];
|
|
|
|
/*
|
|
* Age is tracked in ticks, not from weather_data_t.last_update. That field
|
|
* comes from time(NULL), and with no SNTP client the system clock starts
|
|
* at the epoch -- an age derived from it would be nonsense. Tick
|
|
* subtraction is also wrap-safe.
|
|
*/
|
|
TickType_t last_success = 0;
|
|
int consecutive_failures = 0;
|
|
bool have_reading = false;
|
|
bool stack_logged = false;
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(WEATHER_FIRST_DELAY_MS));
|
|
|
|
while (1) {
|
|
if (!wifi_is_connected()) {
|
|
ui_set_status("WiFi: reconnecting...");
|
|
vTaskDelay(pdMS_TO_TICKS(WEATHER_UPDATE_INTERVAL_MS));
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* Freeze rendering for the duration of the fetch. The TLS session,
|
|
* the LVGL redraw and the flush into the PSRAM framebuffer all
|
|
* compete with the LCD's own DMA out of that same PSRAM; letting them
|
|
* overlap is what produces visible artefacts. The panel keeps
|
|
* scanning the last completed frame, so the screen simply holds still.
|
|
*/
|
|
lvgl_port_pause();
|
|
const esp_err_t err = weather_update(&weather);
|
|
lvgl_port_resume();
|
|
|
|
if (err == ESP_OK) {
|
|
last_success = xTaskGetTickCount();
|
|
consecutive_failures = 0;
|
|
have_reading = true;
|
|
|
|
weather_get_string(&weather, line, sizeof(line));
|
|
ESP_LOGI(TAG, "Weather: %s", line);
|
|
|
|
char detail[64];
|
|
snprintf(detail, sizeof(detail), "%.0f%% humidity",
|
|
weather.has_humidity ? weather.humidity : 0.0f);
|
|
|
|
/* lroundf, not +0.5f-and-truncate: the latter rounds toward zero
|
|
* below 0°F, turning -10.6 into -10. */
|
|
ui_update_weather((int16_t)lroundf(weather.temp_f),
|
|
weather.has_humidity ? detail : "Outdoor");
|
|
ui_set_weather_stale(false);
|
|
|
|
wifi_get_status(line, sizeof(line));
|
|
ui_set_status(line);
|
|
|
|
/* One-shot, after a fetch that included a TLS handshake -- that is
|
|
* the deepest this task's stack ever gets. */
|
|
if (!stack_logged) {
|
|
stack_logged = true;
|
|
ESP_LOGI(TAG, "weather task stack headroom: %u bytes",
|
|
(unsigned)(uxTaskGetStackHighWaterMark(NULL) * sizeof(StackType_t)));
|
|
}
|
|
} else {
|
|
consecutive_failures++;
|
|
ESP_LOGW(TAG, "Weather update failed (%d consecutive)", consecutive_failures);
|
|
|
|
if (have_reading && consecutive_failures >= WEATHER_STALE_AFTER_POLLS) {
|
|
const uint32_t age_min =
|
|
((xTaskGetTickCount() - last_success) * portTICK_PERIOD_MS) / 60000U;
|
|
ui_set_weather_stale(true);
|
|
snprintf(line, sizeof(line), "No update for %lum", (unsigned long)age_min);
|
|
ui_set_status(line);
|
|
} else {
|
|
ui_set_status("Weather update failed");
|
|
}
|
|
}
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(WEATHER_UPDATE_INTERVAL_MS));
|
|
}
|
|
}
|
|
|
|
void app_main(void)
|
|
{
|
|
ESP_LOGI(TAG, "Thermostat HMI -- CrowPanel 7.0\" ESP32-S3 (800x480)");
|
|
|
|
/*
|
|
* WiFi comes up FIRST, before the RGB panel exists.
|
|
*
|
|
* The panel's bounce-buffer ISR reads the framebuffer out of PSRAM, and
|
|
* PSRAM shares the cache with flash on the ESP32-S3. Bringing up NVS and
|
|
* the WiFi PHY writes calibration data to flash, which disables the cache
|
|
* and faults that ISR. Doing it here means every flash write is finished
|
|
* before the panel starts scanning.
|
|
*/
|
|
const esp_err_t wifi_err = wifi_init_sta();
|
|
if (wifi_err != ESP_OK) {
|
|
ESP_LOGE(TAG, "WiFi not up yet -- weather starts when it connects");
|
|
}
|
|
|
|
/* LVGL core must exist before the display and touch drivers register
|
|
* themselves with it. */
|
|
ESP_ERROR_CHECK(lvgl_port_init());
|
|
|
|
/* Shared bus: STC8H1K28 board controller and GT911 both live on it. */
|
|
ESP_ERROR_CHECK(i2c_bus_init());
|
|
|
|
/* Must precede touch_init(): the STC8H1K28 holds the GT911 in reset
|
|
* until its init sequence runs. */
|
|
if (backlight_init() != ESP_OK) {
|
|
ESP_LOGE(TAG, "STC8H1K28 init failed -- backlight and touch will not work");
|
|
}
|
|
i2c_bus_scan(TAG);
|
|
|
|
ESP_ERROR_CHECK(display_init());
|
|
|
|
if (touch_init() != ESP_OK) {
|
|
/* A dead touch panel should not stop the display from working. */
|
|
ESP_LOGW(TAG, "Touch unavailable -- continuing without input");
|
|
}
|
|
|
|
ui_init();
|
|
ESP_ERROR_CHECK(lvgl_port_start());
|
|
|
|
/* First frame is rendered by now, so it is safe to light the panel. */
|
|
vTaskDelay(pdMS_TO_TICKS(100));
|
|
ESP_ERROR_CHECK(backlight_set(255));
|
|
|
|
if (wifi_err == ESP_OK) {
|
|
char status[96];
|
|
wifi_get_status(status, sizeof(status));
|
|
ui_set_status(status);
|
|
} else if (wifi_err == ESP_FAIL || wifi_err == ESP_ERR_TIMEOUT) {
|
|
/* The event handler keeps retrying in the background; the weather
|
|
* task polls wifi_is_connected(), so start it anyway rather than
|
|
* permanently disabling weather because boot raced a slow AP. */
|
|
ui_set_status("WiFi: connecting...");
|
|
} else {
|
|
/* No SSID configured (or setup failed outright) -- nothing will ever
|
|
* connect, so there is no point starting the weather task. */
|
|
ui_set_status("WiFi: not configured");
|
|
return;
|
|
}
|
|
|
|
if (weather_init() != ESP_OK) {
|
|
ui_set_status("Weather: no API credentials");
|
|
return;
|
|
}
|
|
|
|
xTaskCreate(weather_update_task, "weather", WEATHER_TASK_STACK, NULL,
|
|
WEATHER_TASK_PRIO, NULL);
|
|
|
|
ESP_LOGI(TAG, "System ready");
|
|
}
|