Fetch refresh interval from server instead of hardcoding it

Replaces check_server_reachable() (bare bool, GET /health) with
fetch_frame_config(), which GETs the server's new /frame/config endpoint
instead -- doubles as the reachability check (any completed HTTP response
counts, same as before) and delivers the server-configured
refresh_interval_s, used for the success-path deep sleep duration instead
of the Kconfig-only default.

Parses the tiny JSON response with a hand-rolled scalar extractor
(json_extract_uint) rather than pulling in a JSON library -- cJSON isn't
bundled in this ESP-IDF install, and a single flat integer field doesn't
justify a new dependency. Verified standalone against exactly the JSON
shape the server emits, including a missing-field fallback case.

FRAME_SLEEP_INTERVAL_S (Kconfig) is now just the fallback used before the
device has ever reached a configured server, or if the response is
missing/unparseable -- documented as such in its help text.
This commit is contained in:
2026-07-18 15:34:37 -04:00
parent 6e353c2271
commit 21af41ff66
2 changed files with 89 additions and 17 deletions
+14 -7
View File
@@ -45,11 +45,13 @@ menu "ESPresso Frame Configuration"
stored home WiFi network before counting it as a failed retry. stored home WiFi network before counting it as a failed retry.
config FRAME_SERVER_CHECK_TIMEOUT_MS config FRAME_SERVER_CHECK_TIMEOUT_MS
int "Tools server reachability check timeout (ms)" int "Tools server /frame/config request timeout (ms)"
default 3000 default 3000
help help
How long to wait for an HTTP response from the tools server How long to wait for a GET /frame/config response from the
when checking reachability on the post-connect status screen. tools server -- doubles as both the reachability check for the
post-connect status screen and the source of the
server-configured refresh interval below.
config FRAME_FETCH_TIMEOUT_MS config FRAME_FETCH_TIMEOUT_MS
int "Image fetch HTTP timeout (ms)" int "Image fetch HTTP timeout (ms)"
@@ -61,12 +63,17 @@ menu "ESPresso Frame Configuration"
check's timeout. check's timeout.
config FRAME_SLEEP_INTERVAL_S config FRAME_SLEEP_INTERVAL_S
int "Deep sleep interval between refreshes (seconds)" int "Fallback deep sleep interval between refreshes (seconds)"
default 3600 default 3600
help help
How long the device deep-sleeps between successful The refresh interval is normally set server-side (the "Refresh
fetch-and-display cycles. Lower this for bench testing so you interval" field in the server's web UI, delivered via GET
don't have to wait an hour per iteration. /frame/config) so it can be changed without reflashing. This
value is only a fallback: used before the device has ever
successfully reached a configured server, or if the server's
response doesn't include a valid interval (e.g. an older
server version). Lower this for bench testing so you don't
have to wait an hour per iteration.
config FRAME_RETRY_INTERVAL_S config FRAME_RETRY_INTERVAL_S
int "Deep sleep interval after a failed cycle (seconds)" int "Deep sleep interval after a failed cycle (seconds)"
+75 -10
View File
@@ -111,12 +111,52 @@ esp_err_t frame_wifi_connect_sta(const frame_config_t *cfg)
return result; return result;
} }
/* Probes the server's /health endpoint -- any completed HTTP response typedef struct {
* means the socket-level connection succeeded, which is all this checks. */ bool reachable;
static bool check_server_reachable(const char *toolsserver) uint32_t refresh_interval_s; /* CONFIG_FRAME_SLEEP_INTERVAL_S if absent/unparseable */
} frame_server_config_t;
/* Finds the first integer value associated with "key" in a small JSON
* blob, e.g. 3600 in {"refresh_interval_s": 3600}. Not a general JSON
* parser -- just enough for this project's small, flat config response,
* to avoid pulling in a JSON library for one scalar field. */
static bool json_extract_uint(const char *json, const char *key, uint32_t *out)
{ {
char needle[48];
snprintf(needle, sizeof(needle), "\"%s\"", key);
const char *pos = strstr(json, needle);
if (pos == NULL) {
return false;
}
pos = strchr(pos, ':');
if (pos == NULL) {
return false;
}
pos++;
while (*pos == ' ') {
pos++;
}
char *end;
unsigned long value = strtoul(pos, &end, 10);
if (end == pos) {
return false;
}
*out = (uint32_t)value;
return true;
}
/* GETs the server's /frame/config -- doubles as both the reachability
* check (any completed HTTP response means the socket-level connection
* succeeded) and the source of the server-configurable refresh interval. */
static frame_server_config_t fetch_frame_config(const char *toolsserver)
{
frame_server_config_t result = {
.reachable = false,
.refresh_interval_s = CONFIG_FRAME_SLEEP_INTERVAL_S,
};
char url[160]; char url[160];
snprintf(url, sizeof(url), "http://%s/health", toolsserver); snprintf(url, sizeof(url), "http://%s/frame/config", toolsserver);
esp_http_client_config_t config = { esp_http_client_config_t config = {
.url = url, .url = url,
@@ -124,13 +164,38 @@ static bool check_server_reachable(const char *toolsserver)
.timeout_ms = CONFIG_FRAME_SERVER_CHECK_TIMEOUT_MS, .timeout_ms = CONFIG_FRAME_SERVER_CHECK_TIMEOUT_MS,
}; };
esp_http_client_handle_t client = esp_http_client_init(&config); 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);
esp_err_t err = esp_http_client_open(client, 0);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG, "Server '%s' not reachable: %s", toolsserver, esp_err_to_name(err)); ESP_LOGW(TAG, "Server '%s' not reachable: %s", toolsserver, esp_err_to_name(err));
esp_http_client_cleanup(client);
return result;
} }
return err == ESP_OK;
esp_http_client_fetch_headers(client);
result.reachable = true;
char body[256];
int total = 0;
int n;
while (total < (int)sizeof(body) - 1 &&
(n = esp_http_client_read(client, body + total, sizeof(body) - 1 - total)) > 0) {
total += n;
}
body[total] = '\0';
esp_http_client_close(client);
esp_http_client_cleanup(client);
uint32_t interval;
if (json_extract_uint(body, "refresh_interval_s", &interval)) {
result.refresh_interval_s = interval;
} else {
ESP_LOGW(TAG, "'%s' response missing refresh_interval_s, using fallback %ds", url,
(int)result.refresh_interval_s);
}
return result;
} }
typedef struct { typedef struct {
@@ -212,10 +277,10 @@ void frame_client_run(const frame_config_t *cfg)
} }
} }
uint32_t sleep_seconds = CONFIG_FRAME_SLEEP_INTERVAL_S; frame_server_config_t server_cfg = fetch_frame_config(cfg->toolsserver);
bool server_reachable = check_server_reachable(cfg->toolsserver); uint32_t sleep_seconds = server_cfg.refresh_interval_s;
if (!server_reachable) { if (!server_cfg.reachable) {
/* Nothing's been drawn yet this cycle (aside from the optional /* Nothing's been drawn yet this cycle (aside from the optional
* PENDING screen above), so this is cheap diagnostics without * PENDING screen above), so this is cheap diagnostics without
* compounding flashing. */ * compounding flashing. */