Skip redundant panel refreshes and fetch the image before the config check
The panel driver now splits writing a frame into its SPI buffer (epd_write_frame(), which also computes a CRC32 as it streams) from actually triggering the physical refresh (epd_turn_on_display()). frame_client.c compares the new CRC against the last one that was actually refreshed (persisted in NVS) and skips the refresh entirely when they match -- e.g. a reboot redisplaying the same photo before the server's refresh interval elapsed no longer causes a visible flash for no visual change. Also reorders the per-wake fetch cycle: the image fetch (15s timeout) now goes before the config check (3s timeout), instead of after. The config check's tighter timeout was intermittently tripping on connection-setup latency that's common on the first request after waking from a long deep sleep (e.g. stale ARP); putting the more tolerant request first absorbs that latency, and the config check then rides the connection it already warmed up.
This commit is contained in:
+42
-19
@@ -16,22 +16,28 @@ sequenceDiagram
|
|||||||
Note over Frame: User scans WiFi QR, then config QR -> fills in<br/>home WiFi + "Tools Server" host:port
|
Note over Frame: User scans WiFi QR, then config QR -> fills in<br/>home WiFi + "Tools Server" host:port
|
||||||
Frame->>Frame: Save config to NVS, reboot
|
Frame->>Frame: Save config to NVS, reboot
|
||||||
|
|
||||||
Note over Frame: Every wake (deep sleep timer)
|
Note over Frame: Every wake (deep sleep timer, next-photo button,<br/>or any other reboot)
|
||||||
Frame->>Frame: Connect to home WiFi
|
Frame->>Frame: Connect to home WiFi
|
||||||
Frame->>Server: GET /frame/config
|
alt next-photo button pressed
|
||||||
Server-->>Frame: {"refresh_interval_s": ...}
|
Frame->>Server: POST /frame/advance
|
||||||
alt server unreachable
|
Server->>Server: Force-advance to next queued photo, reset interval clock
|
||||||
Frame->>Frame: Show "SERVER: FAILED" status screen
|
else normal wake
|
||||||
Frame->>Frame: Deep sleep (short retry interval)
|
|
||||||
else server reachable
|
|
||||||
Frame->>Server: GET /frame/image
|
Frame->>Server: GET /frame/image
|
||||||
|
Server->>Server: Advance only if refresh_interval_s has elapsed<br/>since the current photo was set -- otherwise a no-op
|
||||||
|
end
|
||||||
Server->>Immich: List album assets / download preview / faces
|
Server->>Immich: List album assets / download preview / faces
|
||||||
Immich-->>Server: JPEG + face bounding boxes
|
Immich-->>Server: JPEG + face bounding boxes
|
||||||
Server->>Server: Crop (face-aware) + quantize (dither) + pack 4bpp
|
Server->>Server: Crop (face-aware) + quantize (dither) + pack 4bpp
|
||||||
Server-->>Frame: 192,000 raw bytes, streamed
|
Server-->>Frame: 192,000 raw bytes, streamed
|
||||||
Frame->>Frame: Stream straight to panel SPI, refresh
|
Frame->>Frame: Write to panel SPI buffer, compute CRC32
|
||||||
Frame->>Frame: Deep sleep (server-configured interval)
|
alt CRC unchanged since last physical refresh
|
||||||
|
Frame->>Frame: Skip refresh (nothing visually changed)
|
||||||
|
else CRC changed
|
||||||
|
Frame->>Frame: Trigger physical refresh, store new CRC
|
||||||
end
|
end
|
||||||
|
Frame->>Server: GET /frame/config
|
||||||
|
Server-->>Frame: {"refresh_interval_s": ...}
|
||||||
|
Frame->>Frame: Deep sleep (server-configured interval, or a short<br/>retry interval on any failure)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Firmware boot flow
|
## Firmware boot flow
|
||||||
@@ -46,19 +52,36 @@ sequenceDiagram
|
|||||||
2. **Stored config exists**: connect to the saved WiFi network (a few
|
2. **Stored config exists**: connect to the saved WiFi network (a few
|
||||||
retries before falling back to provisioning if it fails), then run the
|
retries before falling back to provisioning if it fails), then run the
|
||||||
fetch cycle in `frame_client.c`:
|
fetch cycle in `frame_client.c`:
|
||||||
- `GET /frame/config` on the configured tools server -- doubles as a
|
- Check the next-photo button (`next_button_check()`) -- if it was
|
||||||
reachability check and the source of the refresh interval (a
|
what woke the device (checked via the latched
|
||||||
Kconfig value is only used as a fallback).
|
`esp_sleep_get_gpio_wakeup_status()`, not a live pin read, since a
|
||||||
- If reachable, `GET /frame/image` and stream the response directly
|
quick tap can release before boot gets around to polling it) or is
|
||||||
into the panel over SPI (`epd_display_stream()`), never buffering
|
currently held, the fetch below hits `POST /frame/advance` instead
|
||||||
the full ~192KB frame in RAM.
|
of `GET /frame/image`, forcing the server to skip ahead immediately.
|
||||||
- The panel driver refuses to physically refresh unless the stream
|
- Fetch the frame and write it into the panel's SPI buffer
|
||||||
supplied *exactly* the expected byte count -- a truncated or
|
(`epd_write_frame()`), computing a CRC32 as it streams -- never
|
||||||
wrong-size response leaves the previous image on screen instead of
|
buffering the full ~192KB frame in RAM. The panel driver refuses to
|
||||||
painting garbage.
|
write a short/wrong-size response into the buffer at all, so a
|
||||||
|
truncated fetch can't corrupt what's already there.
|
||||||
|
- Compare the new CRC32 against the last one that was actually
|
||||||
|
refreshed onto the panel (persisted in NVS). If it matches -- the
|
||||||
|
same photo is already visibly on screen, e.g. the device rebooted
|
||||||
|
before the server's refresh interval elapsed -- skip the physical
|
||||||
|
refresh entirely (`epd_turn_on_display()`), avoiding its visible
|
||||||
|
flash and 15-30s duration for no visual change. Otherwise trigger
|
||||||
|
the refresh and store the new CRC.
|
||||||
|
- `GET /frame/config` for the refresh interval, used to set the deep
|
||||||
|
sleep duration -- deliberately fetched *after* the image, not
|
||||||
|
before: its timeout is much tighter (3s vs. the image fetch's 15s),
|
||||||
|
and fetching second lets it ride the connection the image fetch just
|
||||||
|
warmed up rather than eating the latency spike common on the first
|
||||||
|
request after waking from a long sleep.
|
||||||
- Deep sleep for the server-configured interval on success, or a
|
- Deep sleep for the server-configured interval on success, or a
|
||||||
shorter retry interval on any failure.
|
shorter retry interval on any failure.
|
||||||
|
|
||||||
|
The factory-reset button (hold 10s) is checked earlier, before any of
|
||||||
|
this -- see [`firmware/README.md`](../firmware/README.md#resetting-to-provisioning-mode).
|
||||||
|
|
||||||
See [`docs/hardware.md`](hardware.md) for wiring and
|
See [`docs/hardware.md`](hardware.md) for wiring and
|
||||||
[`server/README.md`](../server/README.md) for the server side.
|
[`server/README.md`](../server/README.md) for the server side.
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
#include "esp_check.h"
|
#include "esp_check.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
|
#include "esp_rom_crc.h"
|
||||||
|
|
||||||
#include "epd7in3e.h"
|
#include "epd7in3e.h"
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ static void epd_reset(void)
|
|||||||
|
|
||||||
/* Power on, "second setting" registers, refresh, power off -- mirrors
|
/* Power on, "second setting" registers, refresh, power off -- mirrors
|
||||||
* EPD_7IN3E_TurnOnDisplay() in the reference driver. */
|
* EPD_7IN3E_TurnOnDisplay() in the reference driver. */
|
||||||
static esp_err_t epd_turn_on_display(void)
|
esp_err_t epd_turn_on_display(void)
|
||||||
{
|
{
|
||||||
EPD_CHECK(epd_send_command(0x04)); // POWER_ON
|
EPD_CHECK(epd_send_command(0x04)); // POWER_ON
|
||||||
epd_wait_busy();
|
epd_wait_busy();
|
||||||
@@ -204,7 +205,7 @@ esp_err_t epd_init(void)
|
|||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t epd_display_stream(epd_read_fn_t read_fn, void *ctx)
|
esp_err_t epd_write_frame(epd_read_fn_t read_fn, void *ctx, uint32_t *out_crc32)
|
||||||
{
|
{
|
||||||
ESP_RETURN_ON_FALSE(read_fn != NULL, ESP_ERR_INVALID_ARG, TAG, "read_fn required");
|
ESP_RETURN_ON_FALSE(read_fn != NULL, ESP_ERR_INVALID_ARG, TAG, "read_fn required");
|
||||||
|
|
||||||
@@ -220,6 +221,7 @@ esp_err_t epd_display_stream(epd_read_fn_t read_fn, void *ctx)
|
|||||||
* reentrant, but this driver only ever runs from one task at a time. */
|
* reentrant, but this driver only ever runs from one task at a time. */
|
||||||
static uint8_t chunk[EPD_SPI_CHUNK_SIZE];
|
static uint8_t chunk[EPD_SPI_CHUNK_SIZE];
|
||||||
size_t total = 0;
|
size_t total = 0;
|
||||||
|
uint32_t crc = 0;
|
||||||
size_t n;
|
size_t n;
|
||||||
esp_err_t err = ESP_OK;
|
esp_err_t err = ESP_OK;
|
||||||
while ((n = read_fn(chunk, sizeof(chunk), ctx)) > 0) {
|
while ((n = read_fn(chunk, sizeof(chunk), ctx)) > 0) {
|
||||||
@@ -227,6 +229,7 @@ esp_err_t epd_display_stream(epd_read_fn_t read_fn, void *ctx)
|
|||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
crc = esp_rom_crc32_le(crc, chunk, n);
|
||||||
total += n;
|
total += n;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,18 +238,31 @@ esp_err_t epd_display_stream(epd_read_fn_t read_fn, void *ctx)
|
|||||||
|
|
||||||
if (total != EPD_FRAME_BYTES) {
|
if (total != EPD_FRAME_BYTES) {
|
||||||
/* Whatever was received has already been clocked into the panel's
|
/* Whatever was received has already been clocked into the panel's
|
||||||
* internal RAM over SPI, but epd_turn_on_display() (the actual
|
* internal RAM over SPI, but the physical refresh trigger hasn't
|
||||||
* physical refresh trigger) hasn't been called yet -- returning
|
* been called -- returning here instead leaves the visible screen
|
||||||
* here instead leaves the visible screen exactly as it was, rather
|
* exactly as it was, rather than refreshing onto a mostly-garbage
|
||||||
* than refreshing onto a mostly-garbage buffer. Confirmed on
|
* buffer. Confirmed on hardware: a misdirected fetch that returned
|
||||||
* hardware: a misdirected fetch that returned a ~10KB error page
|
* a ~10KB error page instead of a 192,000-byte frame still
|
||||||
* instead of a 192,000-byte frame still triggered a refresh before
|
* triggered a refresh before this check existed, painting garbage
|
||||||
* this check existed, painting garbage over a previously-good image. */
|
* over a previously-good image. */
|
||||||
ESP_LOGE(TAG, "Stream supplied %u bytes, expected %u -- aborting refresh",
|
ESP_LOGE(TAG, "Stream supplied %u bytes, expected %u -- aborting refresh",
|
||||||
(unsigned)total, (unsigned)EPD_FRAME_BYTES);
|
(unsigned)total, (unsigned)EPD_FRAME_BYTES);
|
||||||
return ESP_ERR_INVALID_SIZE;
|
return ESP_ERR_INVALID_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (out_crc32 != NULL) {
|
||||||
|
*out_crc32 = crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t epd_display_stream(epd_read_fn_t read_fn, void *ctx)
|
||||||
|
{
|
||||||
|
esp_err_t err = epd_write_frame(read_fn, ctx, NULL);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
return epd_turn_on_display();
|
return epd_turn_on_display();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,29 @@ typedef size_t (*epd_read_fn_t)(uint8_t *chunk, size_t chunk_size, void *ctx);
|
|||||||
*/
|
*/
|
||||||
esp_err_t epd_display_stream(epd_read_fn_t read_fn, void *ctx);
|
esp_err_t epd_display_stream(epd_read_fn_t read_fn, void *ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like epd_display_stream(), but writes the frame into the panel's
|
||||||
|
* internal buffer over SPI WITHOUT triggering the physical refresh (the
|
||||||
|
* visible flash/flicker, which also takes 15-30+ seconds) -- call
|
||||||
|
* epd_turn_on_display() separately to make it visible. Returns
|
||||||
|
* ESP_ERR_INVALID_SIZE if read_fn didn't supply exactly EPD_FRAME_BYTES,
|
||||||
|
* same as epd_display_stream(); either way nothing is refreshed, so the
|
||||||
|
* visible screen is left untouched on error.
|
||||||
|
*
|
||||||
|
* If out_crc32 is non-NULL, it's set to a CRC32 of the bytes written --
|
||||||
|
* lets a caller compare against the last-displayed frame's CRC and skip
|
||||||
|
* the refresh entirely when nothing actually changed (e.g. redisplaying
|
||||||
|
* the same photo after a reboot).
|
||||||
|
*/
|
||||||
|
esp_err_t epd_write_frame(epd_read_fn_t read_fn, void *ctx, uint32_t *out_crc32);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers the panel's physical refresh cycle (power on, refresh, power
|
||||||
|
* off) -- the visible flash/flicker sequence, 15-30+ seconds. Call after
|
||||||
|
* epd_write_frame() to make the written buffer visible.
|
||||||
|
*/
|
||||||
|
esp_err_t epd_turn_on_display(void);
|
||||||
|
|
||||||
/** Convenience wrapper around epd_display_stream() for an in-memory frame buffer. */
|
/** Convenience wrapper around epd_display_stream() for an in-memory frame buffer. */
|
||||||
esp_err_t epd_display_buffer(const uint8_t *frame, size_t len);
|
esp_err_t epd_display_buffer(const uint8_t *frame, size_t len);
|
||||||
|
|
||||||
|
|||||||
@@ -248,11 +248,30 @@ static esp_err_t fetch_and_display(const frame_config_t *cfg, bool force_advance
|
|||||||
ESP_LOGI(TAG, "Fetching frame (%d bytes) from '%s'", content_length, url);
|
ESP_LOGI(TAG, "Fetching frame (%d bytes) from '%s'", content_length, url);
|
||||||
|
|
||||||
http_read_ctx_t ctx = { .client = client };
|
http_read_ctx_t ctx = { .client = client };
|
||||||
err = epd_display_stream(http_read_fn, &ctx);
|
uint32_t crc = 0;
|
||||||
|
err = epd_write_frame(http_read_fn, &ctx, &crc);
|
||||||
|
|
||||||
esp_http_client_close(client);
|
esp_http_client_close(client);
|
||||||
esp_http_client_cleanup(client);
|
esp_http_client_cleanup(client);
|
||||||
|
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t previous_crc;
|
||||||
|
if (frame_config_get_last_display_crc32(&previous_crc) == ESP_OK && previous_crc == crc) {
|
||||||
|
/* Same photo already on screen (e.g. redisplayed after a reboot,
|
||||||
|
* before the refresh interval elapsed server-side) -- skip the
|
||||||
|
* physical refresh, avoiding its visible flash and 15-30s
|
||||||
|
* duration for no visual change. */
|
||||||
|
ESP_LOGI(TAG, "Frame unchanged since last display, skipping refresh");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = epd_turn_on_display();
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
frame_config_set_last_display_crc32(crc);
|
||||||
|
}
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,35 +295,43 @@ void frame_client_run(const frame_config_t *cfg, bool force_advance)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
frame_server_config_t server_cfg = fetch_frame_config(cfg->toolsserver);
|
/* The image fetch goes before the config check, not after. It has a
|
||||||
uint32_t sleep_seconds = server_cfg.refresh_interval_s;
|
* far more generous timeout (CONFIG_FRAME_FETCH_TIMEOUT_MS, 15s by
|
||||||
|
* default, vs. the config check's 3s), so it comfortably absorbs the
|
||||||
if (!server_cfg.reachable) {
|
* extra connection-setup latency that's common on the very first
|
||||||
/* Nothing's been drawn yet this cycle (aside from the optional
|
* request after waking from a long deep sleep (stale ARP entries and
|
||||||
* PENDING screen above), so this is cheap diagnostics without
|
* the like) -- confirmed on hardware: the config check's tight
|
||||||
* compounding flashing. */
|
* timeout was intermittently tripping on exactly that latency while
|
||||||
ESP_LOGW(TAG, "Tools server not reachable, retrying sooner");
|
* it went first, even though the image fetch right after it (on an
|
||||||
sleep_seconds = CONFIG_FRAME_RETRY_INTERVAL_S;
|
* already-warm connection) never had trouble. Trade-off: on a fully
|
||||||
if (have_display) {
|
* down server, the device now waits up to the image fetch's longer
|
||||||
status_screen_show(cfg->sta_ssid, STATUS_OK, cfg->toolsserver, STATUS_FAILED);
|
* timeout to notice, instead of the config check's shorter one --
|
||||||
}
|
* worth it to stop false-failing on the common case. */
|
||||||
} else {
|
bool image_ok = true;
|
||||||
if (first_connection && have_display) {
|
|
||||||
status_screen_show(cfg->sta_ssid, STATUS_OK, cfg->toolsserver, STATUS_OK);
|
|
||||||
}
|
|
||||||
if (have_display) {
|
if (have_display) {
|
||||||
esp_err_t fetch_err = fetch_and_display(cfg, force_advance);
|
esp_err_t fetch_err = fetch_and_display(cfg, force_advance);
|
||||||
if (fetch_err != ESP_OK) {
|
image_ok = (fetch_err == ESP_OK);
|
||||||
/* epd_display_stream() never triggers a physical refresh on
|
if (!image_ok) {
|
||||||
* a failed/short/wrong-size stream (see epd7in3e.c), so the
|
/* epd_display_stream() never triggers a physical refresh on a
|
||||||
* visible screen is guaranteed untouched here -- always
|
* failed/short/wrong-size stream (see epd7in3e.c), so the
|
||||||
* safe to show what went wrong instead of leaving stale
|
* visible screen is guaranteed untouched here -- always safe
|
||||||
* content with no indication anything failed. */
|
* to show what went wrong instead of leaving stale content
|
||||||
|
* with no indication anything failed. */
|
||||||
ESP_LOGW(TAG, "Fetch/display failed (%s), retrying sooner", esp_err_to_name(fetch_err));
|
ESP_LOGW(TAG, "Fetch/display failed (%s), retrying sooner", esp_err_to_name(fetch_err));
|
||||||
sleep_seconds = CONFIG_FRAME_RETRY_INTERVAL_S;
|
|
||||||
status_screen_show(cfg->sta_ssid, STATUS_OK, cfg->toolsserver, STATUS_FAILED);
|
status_screen_show(cfg->sta_ssid, STATUS_OK, cfg->toolsserver, STATUS_FAILED);
|
||||||
|
} else if (first_connection) {
|
||||||
|
status_screen_show(cfg->sta_ssid, STATUS_OK, cfg->toolsserver, STATUS_OK);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Only worth asking for the refresh interval if the image fetch
|
||||||
|
* actually worked -- a failed fetch already means CONFIG_FRAME_RETRY_INTERVAL_S,
|
||||||
|
* so there's nothing to gain from a config request whose result would
|
||||||
|
* just be discarded. */
|
||||||
|
uint32_t sleep_seconds = CONFIG_FRAME_RETRY_INTERVAL_S;
|
||||||
|
if (image_ok) {
|
||||||
|
frame_server_config_t server_cfg = fetch_frame_config(cfg->toolsserver);
|
||||||
|
sleep_seconds = server_cfg.reachable ? server_cfg.refresh_interval_s : CONFIG_FRAME_RETRY_INTERVAL_S;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (have_display) {
|
if (have_display) {
|
||||||
|
|||||||
@@ -144,6 +144,29 @@ void frame_config_clear(void)
|
|||||||
nvs_close(handle);
|
nvs_close(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
esp_err_t frame_config_get_last_display_crc32(uint32_t *out)
|
||||||
|
{
|
||||||
|
nvs_handle_t handle;
|
||||||
|
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
err = nvs_get_u32(handle, "last_crc32", out);
|
||||||
|
nvs_close(handle);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
void frame_config_set_last_display_crc32(uint32_t crc32)
|
||||||
|
{
|
||||||
|
nvs_handle_t handle;
|
||||||
|
if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle) != ESP_OK) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nvs_set_u32(handle, "last_crc32", crc32);
|
||||||
|
nvs_commit(handle);
|
||||||
|
nvs_close(handle);
|
||||||
|
}
|
||||||
|
|
||||||
static void generate_ap_password(char *out, size_t out_size)
|
static void generate_ap_password(char *out, size_t out_size)
|
||||||
{
|
{
|
||||||
size_t len = MIN(FRAME_AP_PASSWORD_LEN, out_size - 1);
|
size_t len = MIN(FRAME_AP_PASSWORD_LEN, out_size - 1);
|
||||||
|
|||||||
@@ -41,10 +41,22 @@ void frame_config_mark_connected_once(void);
|
|||||||
* device falls back into provisioning on its next boot. Leaves the softAP
|
* device falls back into provisioning on its next boot. Leaves the softAP
|
||||||
* identity (SSID/password) untouched, since that's tied to the device
|
* identity (SSID/password) untouched, since that's tied to the device
|
||||||
* itself, not a particular home network -- regenerating it on every reset
|
* itself, not a particular home network -- regenerating it on every reset
|
||||||
* would force re-scanning the join QR code for no reason.
|
* would force re-scanning the join QR code for no reason. Also leaves the
|
||||||
|
* last-displayed-photo CRC (below) untouched -- it describes what's
|
||||||
|
* physically on screen, not network config, and stays valid regardless.
|
||||||
*/
|
*/
|
||||||
void frame_config_clear(void);
|
void frame_config_clear(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the CRC32 of the last frame actually written to the panel via a
|
||||||
|
* physical refresh. Returns ESP_ERR_NVS_NOT_FOUND if nothing's been
|
||||||
|
* displayed yet.
|
||||||
|
*/
|
||||||
|
esp_err_t frame_config_get_last_display_crc32(uint32_t *out);
|
||||||
|
|
||||||
|
/** Records the CRC32 of the frame just displayed, for next time. */
|
||||||
|
void frame_config_set_last_display_crc32(uint32_t crc32);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns this device's provisioning AP identity: a fixed SSID (from
|
* Returns this device's provisioning AP identity: a fixed SSID (from
|
||||||
* Kconfig) and a password that's generated once on first use and persisted
|
* Kconfig) and a password that's generated once on first use and persisted
|
||||||
|
|||||||
Reference in New Issue
Block a user