#include #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(); }