Two bugs found testing against a real (partially-configured) server: - The reachability check hit HEAD / with -- our server only registers GET on that route, so it always got a 405. Harmless for the check itself (any completed HTTP response counts as "reachable"), but noisy and semantically wrong. Points at GET /health instead, which exists for exactly this. - fetch_and_display() failing before any pixel data was sent (e.g. a 400/404 on /frame/image) was treated the same as a mid-stream failure, which skips drawing a status screen to avoid compounding flashing on top of an already-refreshed panel. But a pre-stream failure never touches the panel at all, so skipping the status screen there just left the old provisioning QR code on screen with no indication anything had gone wrong. fetch_and_display() now reports whether streaming ever started so the caller can tell the two cases apart. - The status screen now always shows on the very first successful connection after (re)provisioning, regardless of outcome, via a new "connected_once" NVS flag that frame_config_save() resets on every fresh provisioning event. Later wakes skip it on success (straight to the photo) but still show it on any failure, matching the intent from the original status-screen feature.
258 lines
9.7 KiB
C
258 lines
9.7 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 the server's /health endpoint -- any completed HTTP response
|
|
* means the socket-level connection succeeded, which is all this checks. */
|
|
static bool check_server_reachable(const char *toolsserver)
|
|
{
|
|
char url[160];
|
|
snprintf(url, sizeof(url), "http://%s/health", toolsserver);
|
|
|
|
esp_http_client_config_t config = {
|
|
.url = url,
|
|
.method = HTTP_METHOD_GET,
|
|
.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.
|
|
* *out_started_stream tells the caller whether the panel was actually
|
|
* touched -- false means the failure happened before any pixel data was
|
|
* sent (bad status, connection error), so nothing was drawn this cycle. */
|
|
static esp_err_t fetch_and_display(const frame_config_t *cfg, bool *out_started_stream)
|
|
{
|
|
*out_started_stream = false;
|
|
|
|
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);
|
|
|
|
*out_started_stream = true;
|
|
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));
|
|
}
|
|
|
|
/* Always show the status screen on the first successful connection
|
|
* after (re)provisioning, regardless of outcome -- confirms the
|
|
* connection worked. Skipped on later wakes to save a refresh, except
|
|
* when something's actually wrong (handled below). */
|
|
bool first_connection = !frame_config_has_connected_once();
|
|
if (first_connection) {
|
|
frame_config_mark_connected_once();
|
|
if (have_display) {
|
|
status_screen_show(cfg->sta_ssid, STATUS_OK, cfg->toolsserver, STATUS_PENDING);
|
|
}
|
|
}
|
|
|
|
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 (aside from the optional
|
|
* PENDING screen above), so this is 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 (first_connection && have_display) {
|
|
status_screen_show(cfg->sta_ssid, STATUS_OK, cfg->toolsserver, STATUS_OK);
|
|
}
|
|
if (have_display) {
|
|
bool stream_started = false;
|
|
esp_err_t fetch_err = fetch_and_display(cfg, &stream_started);
|
|
if (fetch_err != ESP_OK) {
|
|
ESP_LOGW(TAG, "Fetch/display failed (%s), retrying sooner", esp_err_to_name(fetch_err));
|
|
sleep_seconds = CONFIG_FRAME_RETRY_INTERVAL_S;
|
|
if (!stream_started) {
|
|
/* The panel was never touched this cycle (e.g. a bad
|
|
* HTTP status before any pixel data was sent) -- show
|
|
* the failure instead of leaving whatever was on
|
|
* screen before. A mid-stream failure, by contrast,
|
|
* already refreshed the panel with something, so
|
|
* skip compounding it with a second refresh here. */
|
|
status_screen_show(cfg->sta_ssid, STATUS_OK, cfg->toolsserver, STATUS_FAILED);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|