frame_client_run() now does what it was always meant to: probe the tools server, GET /frame/image and stream the response straight into the panel via epd_display_stream() (esp_http_client's manual open/fetch_headers/read API pulls in exactly the shape epd_display_stream()'s read_fn expects, so the ~192KB frame never sits in RAM at once), then epd_sleep() and esp_deep_sleep_start() for an hour. Skips the WiFi/server status checklist screen on the happy path now that there's a real photo to show instead -- three full refreshes every single hour (status-pending, status-final, photo) wasn't worth it once bring-up was actually working. Still shows it (status FAILED) when the server isn't reachable, since nothing's been drawn yet that cycle and it's the cheapest useful diagnostic. A mid-fetch failure after the panel's already started refreshing just logs and retries sooner, rather than compounding with a second refresh. New Kconfig knobs: FRAME_FETCH_TIMEOUT_MS, FRAME_SLEEP_INTERVAL_S (default 3600s), FRAME_RETRY_INTERVAL_S (default 300s on failure).
228 lines
8.3 KiB
C
228 lines
8.3 KiB
C
#include <string.h>
|
|
|
|
#include "esp_event.h"
|
|
#include "esp_log.h"
|
|
#include "esp_wifi.h"
|
|
#include "esp_wifi_default.h"
|
|
#include "esp_netif.h"
|
|
#include "esp_http_client.h"
|
|
#include "esp_sleep.h"
|
|
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/event_groups.h"
|
|
|
|
#include "epd7in3e.h"
|
|
#include "status_screen.h"
|
|
|
|
#include "frame_client.h"
|
|
|
|
static const char *TAG = "frame_client";
|
|
|
|
#define STA_CONNECTED_BIT BIT0
|
|
#define STA_FAILED_BIT BIT1
|
|
|
|
static EventGroupHandle_t s_sta_event_group;
|
|
|
|
/* wifi_sta_config_t's ssid/password fields are fixed-size byte arrays, not
|
|
* necessarily null-terminated (a full 32-char SSID fills the field exactly).
|
|
* snprintf() flags that as a possible truncation at -Werror, so copy by
|
|
* hand instead. */
|
|
static void copy_wifi_field(uint8_t *dst, size_t dst_size, const char *src)
|
|
{
|
|
size_t len = strnlen(src, dst_size);
|
|
memcpy(dst, src, len);
|
|
if (len < dst_size) {
|
|
dst[len] = '\0';
|
|
}
|
|
}
|
|
|
|
static void sta_event_handler(void *arg, esp_event_base_t event_base,
|
|
int32_t event_id, void *event_data)
|
|
{
|
|
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
|
|
esp_wifi_connect();
|
|
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
|
ESP_LOGW(TAG, "Disconnected from home WiFi");
|
|
xEventGroupSetBits(s_sta_event_group, STA_FAILED_BIT);
|
|
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
|
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
|
|
ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&event->ip_info.ip));
|
|
xEventGroupSetBits(s_sta_event_group, STA_CONNECTED_BIT);
|
|
}
|
|
}
|
|
|
|
esp_err_t frame_wifi_connect_sta(const frame_config_t *cfg)
|
|
{
|
|
s_sta_event_group = xEventGroupCreate();
|
|
|
|
esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
|
|
|
|
wifi_init_config_t init_cfg = WIFI_INIT_CONFIG_DEFAULT();
|
|
ESP_ERROR_CHECK(esp_wifi_init(&init_cfg));
|
|
|
|
esp_event_handler_instance_t wifi_handler;
|
|
esp_event_handler_instance_t ip_handler;
|
|
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &sta_event_handler, NULL, &wifi_handler));
|
|
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &sta_event_handler, NULL, &ip_handler));
|
|
|
|
wifi_config_t wifi_config = {0};
|
|
copy_wifi_field(wifi_config.sta.ssid, sizeof(wifi_config.sta.ssid), cfg->sta_ssid);
|
|
copy_wifi_field(wifi_config.sta.password, sizeof(wifi_config.sta.password), cfg->sta_password);
|
|
|
|
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
|
|
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
|
ESP_ERROR_CHECK(esp_wifi_start());
|
|
|
|
esp_err_t result = ESP_FAIL;
|
|
for (int attempt = 1; attempt <= CONFIG_FRAME_STA_CONNECT_MAX_RETRIES; attempt++) {
|
|
ESP_LOGI(TAG, "Connecting to '%s' (attempt %d/%d)", cfg->sta_ssid, attempt,
|
|
CONFIG_FRAME_STA_CONNECT_MAX_RETRIES);
|
|
|
|
xEventGroupClearBits(s_sta_event_group, STA_CONNECTED_BIT | STA_FAILED_BIT);
|
|
esp_wifi_connect();
|
|
|
|
EventBits_t bits = xEventGroupWaitBits(s_sta_event_group, STA_CONNECTED_BIT | STA_FAILED_BIT,
|
|
pdTRUE, pdFALSE,
|
|
pdMS_TO_TICKS(CONFIG_FRAME_STA_CONNECT_TIMEOUT_MS));
|
|
|
|
if (bits & STA_CONNECTED_BIT) {
|
|
result = ESP_OK;
|
|
break;
|
|
}
|
|
ESP_LOGW(TAG, "Attempt %d/%d failed", attempt, CONFIG_FRAME_STA_CONNECT_MAX_RETRIES);
|
|
}
|
|
|
|
esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_handler);
|
|
esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, ip_handler);
|
|
vEventGroupDelete(s_sta_event_group);
|
|
s_sta_event_group = NULL;
|
|
|
|
if (result != ESP_OK) {
|
|
/* Fully tear the WiFi driver back down on failure -- the caller
|
|
* falls back to provisioning, which calls esp_wifi_init() again
|
|
* for AP mode. Leaving the driver merely stopped (rather than
|
|
* deinitialized) made that second esp_wifi_init() call fail with
|
|
* ESP_ERR_INVALID_STATE and abort, confirmed on hardware. */
|
|
esp_wifi_stop();
|
|
esp_wifi_deinit();
|
|
esp_netif_destroy_default_wifi(sta_netif);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/* Probes cfg->toolsserver with a plain HTTP HEAD -- any HTTP response
|
|
* (even a 404, since there's no server yet to define real routes) means
|
|
* the socket-level connection succeeded, which is all this is checking. */
|
|
static bool check_server_reachable(const char *toolsserver)
|
|
{
|
|
char url[160];
|
|
snprintf(url, sizeof(url), "http://%s/", toolsserver);
|
|
|
|
esp_http_client_config_t config = {
|
|
.url = url,
|
|
.method = HTTP_METHOD_HEAD,
|
|
.timeout_ms = CONFIG_FRAME_SERVER_CHECK_TIMEOUT_MS,
|
|
};
|
|
esp_http_client_handle_t client = esp_http_client_init(&config);
|
|
esp_err_t err = esp_http_client_perform(client);
|
|
esp_http_client_cleanup(client);
|
|
|
|
if (err != ESP_OK) {
|
|
ESP_LOGW(TAG, "Server '%s' not reachable: %s", toolsserver, esp_err_to_name(err));
|
|
}
|
|
return err == ESP_OK;
|
|
}
|
|
|
|
typedef struct {
|
|
esp_http_client_handle_t client;
|
|
} http_read_ctx_t;
|
|
|
|
/* Pulls the next chunk straight out of the in-progress HTTP response --
|
|
* epd_display_stream() calls this to feed the panel without ever holding
|
|
* the full ~192KB frame in RAM. */
|
|
static size_t http_read_fn(uint8_t *chunk, size_t chunk_size, void *ctx_)
|
|
{
|
|
http_read_ctx_t *ctx = (http_read_ctx_t *)ctx_;
|
|
int n = esp_http_client_read(ctx->client, (char *)chunk, (int)chunk_size);
|
|
return n > 0 ? (size_t)n : 0;
|
|
}
|
|
|
|
/* GETs /frame/image and streams the response directly into the panel. */
|
|
static esp_err_t fetch_and_display(const frame_config_t *cfg)
|
|
{
|
|
char url[160];
|
|
snprintf(url, sizeof(url), "http://%s/frame/image", cfg->toolsserver);
|
|
|
|
esp_http_client_config_t config = {
|
|
.url = url,
|
|
.method = HTTP_METHOD_GET,
|
|
.timeout_ms = CONFIG_FRAME_FETCH_TIMEOUT_MS,
|
|
};
|
|
esp_http_client_handle_t client = esp_http_client_init(&config);
|
|
|
|
esp_err_t err = esp_http_client_open(client, 0);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to open '%s': %s", url, esp_err_to_name(err));
|
|
esp_http_client_cleanup(client);
|
|
return err;
|
|
}
|
|
|
|
int content_length = esp_http_client_fetch_headers(client);
|
|
int status = esp_http_client_get_status_code(client);
|
|
if (status != 200) {
|
|
ESP_LOGE(TAG, "'%s' returned HTTP %d", url, status);
|
|
esp_http_client_close(client);
|
|
esp_http_client_cleanup(client);
|
|
return ESP_FAIL;
|
|
}
|
|
ESP_LOGI(TAG, "Fetching frame (%d bytes) from '%s'", content_length, url);
|
|
|
|
http_read_ctx_t ctx = { .client = client };
|
|
err = epd_display_stream(http_read_fn, &ctx);
|
|
|
|
esp_http_client_close(client);
|
|
esp_http_client_cleanup(client);
|
|
|
|
return err;
|
|
}
|
|
|
|
void frame_client_run(const frame_config_t *cfg)
|
|
{
|
|
esp_err_t epd_err = epd_init();
|
|
bool have_display = (epd_err == ESP_OK);
|
|
if (!have_display) {
|
|
ESP_LOGW(TAG, "EPD init failed (%s), continuing without display", esp_err_to_name(epd_err));
|
|
}
|
|
|
|
uint32_t sleep_seconds = CONFIG_FRAME_SLEEP_INTERVAL_S;
|
|
bool server_reachable = check_server_reachable(cfg->toolsserver);
|
|
|
|
if (!server_reachable) {
|
|
/* Nothing's been drawn yet this cycle, so this is the only
|
|
* refresh -- cheap diagnostics without compounding flashing. */
|
|
ESP_LOGW(TAG, "Tools server not reachable, retrying sooner");
|
|
sleep_seconds = CONFIG_FRAME_RETRY_INTERVAL_S;
|
|
if (have_display) {
|
|
status_screen_show(cfg->sta_ssid, STATUS_OK, cfg->toolsserver, STATUS_FAILED);
|
|
}
|
|
} else if (have_display) {
|
|
esp_err_t fetch_err = fetch_and_display(cfg);
|
|
if (fetch_err != ESP_OK) {
|
|
/* The panel may have already partially refreshed with a
|
|
* truncated frame by this point -- log and retry sooner
|
|
* rather than drawing a second status screen on top of it. */
|
|
ESP_LOGW(TAG, "Fetch/display failed (%s), retrying sooner", esp_err_to_name(fetch_err));
|
|
sleep_seconds = CONFIG_FRAME_RETRY_INTERVAL_S;
|
|
}
|
|
}
|
|
|
|
if (have_display) {
|
|
epd_sleep();
|
|
}
|
|
|
|
ESP_LOGI(TAG, "Deep sleeping for %u seconds", (unsigned)sleep_seconds);
|
|
esp_sleep_enable_timer_wakeup((uint64_t)sleep_seconds * 1000000ULL);
|
|
esp_deep_sleep_start();
|
|
}
|