faster image processing
All checks were successful
ESP32 Build & Release / build (push) Successful in 1m16s

This commit is contained in:
2026-09-13 23:31:36 +02:00
parent 8d034ed1df
commit e6760e4866
3 changed files with 169 additions and 11 deletions

View File

@@ -37,6 +37,8 @@ static const char *TAG = "scryfall";
#define EVT_CARD_CACHED (1U << 0) #define EVT_CARD_CACHED (1U << 0)
#define EVT_CARD_FAILED (1U << 1) #define EVT_CARD_FAILED (1U << 1)
#define EVT_IMG_CACHED (1U << 2)
#define EVT_IMG_FAILED (1U << 3)
typedef struct { typedef struct {
char name[CARD_NAME_MAX]; char name[CARD_NAME_MAX];
@@ -49,6 +51,11 @@ static TaskHandle_t s_net_task;
static volatile uint32_t s_result_bits = 0; static volatile uint32_t s_result_bits = 0;
static char s_last_uid[UID_MAX] = {0}; static char s_last_uid[UID_MAX] = {0};
static volatile uint32_t s_image_bits = 0;
static char s_last_img_uid[UID_MAX] = {0};
static volatile bool s_image_fetching = false;
static char s_fetching_img_uid[UID_MAX] = {0};
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* URL helpers */ /* URL helpers */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@@ -181,7 +188,8 @@ static esp_err_t http_request(const char *url, http_sink_t *sink)
.timeout_ms = HTTP_TIMEOUT_MS, .timeout_ms = HTTP_TIMEOUT_MS,
.event_handler = http_event_handler, .event_handler = http_event_handler,
.user_data = sink, .user_data = sink,
.buffer_size = 2048, .buffer_size = 4096,
.buffer_size_tx = 1024,
.crt_bundle_attach = esp_crt_bundle_attach, .crt_bundle_attach = esp_crt_bundle_attach,
.keep_alive_enable = false, .keep_alive_enable = false,
}; };
@@ -271,7 +279,7 @@ static const char *get_string(const cJSON *item, const char *fallback)
/* Cache metadata */ /* Cache metadata */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
static esp_err_t scryfall_do_fetch(const char *card_name, const char *uid) static esp_err_t scryfall_do_fetch(const char *card_name, const char *uid, bool signal_ui)
{ {
char json_path[64]; char json_path[64];
snprintf(json_path, sizeof(json_path), STORAGE_CARDS_DIR "/%s.json", uid); snprintf(json_path, sizeof(json_path), STORAGE_CARDS_DIR "/%s.json", uid);
@@ -302,9 +310,19 @@ static esp_err_t scryfall_do_fetch(const char *card_name, const char *uid)
if (valid) { if (valid) {
if (storage_file_exists(jpg_path)) { if (storage_file_exists(jpg_path)) {
ESP_LOGI(TAG, "cache hit %s and %s", json_path, jpg_path); ESP_LOGI(TAG, "cache hit %s and %s", json_path, jpg_path);
if (signal_ui) {
snprintf(s_last_uid, sizeof(s_last_uid), "%s", uid);
s_result_bits = EVT_CARD_CACHED;
snprintf(s_last_img_uid, sizeof(s_last_img_uid), "%s", uid);
s_image_bits = EVT_IMG_CACHED;
}
return ESP_OK; return ESP_OK;
} }
ESP_LOGI(TAG, "json valid but %s missing, fetching art...", jpg_path); ESP_LOGI(TAG, "json valid but %s missing, fetching art...", jpg_path);
if (signal_ui) {
snprintf(s_last_uid, sizeof(s_last_uid), "%s", uid);
s_result_bits = EVT_CARD_CACHED;
}
} else { } else {
ESP_LOGW(TAG, "cache entry %s is invalid or card mismatch (expected '%s'), re-fetching...", json_path, card_name); ESP_LOGW(TAG, "cache entry %s is invalid or card mismatch (expected '%s'), re-fetching...", json_path, card_name);
remove(json_path); remove(json_path);
@@ -323,6 +341,10 @@ static esp_err_t scryfall_do_fetch(const char *card_name, const char *uid)
if (ret != ESP_OK || body == NULL) { if (ret != ESP_OK || body == NULL) {
free(body); free(body);
ESP_LOGE(TAG, "scryfall request failed for '%s'", card_name); ESP_LOGE(TAG, "scryfall request failed for '%s'", card_name);
if (signal_ui) {
snprintf(s_last_uid, sizeof(s_last_uid), "%s", uid);
s_result_bits = EVT_CARD_FAILED;
}
return ret != ESP_OK ? ret : ESP_FAIL; return ret != ESP_OK ? ret : ESP_FAIL;
} }
@@ -330,11 +352,19 @@ static esp_err_t scryfall_do_fetch(const char *card_name, const char *uid)
free(body); free(body);
if (root == NULL) { if (root == NULL) {
ESP_LOGE(TAG, "JSON parse failed for '%s'", card_name); ESP_LOGE(TAG, "JSON parse failed for '%s'", card_name);
if (signal_ui) {
snprintf(s_last_uid, sizeof(s_last_uid), "%s", uid);
s_result_bits = EVT_CARD_FAILED;
}
return ESP_FAIL; return ESP_FAIL;
} }
cJSON *obj = cJSON_GetObjectItem(root, "object"); cJSON *obj = cJSON_GetObjectItem(root, "object");
if (obj && cJSON_IsString(obj) && strcmp(obj->valuestring, "error") == 0) { if (obj && cJSON_IsString(obj) && strcmp(obj->valuestring, "error") == 0) {
if (signal_ui) {
snprintf(s_last_uid, sizeof(s_last_uid), "%s", uid);
s_result_bits = EVT_CARD_FAILED;
}
ESP_LOGE(TAG, "Scryfall API error: %s", ESP_LOGE(TAG, "Scryfall API error: %s",
get_string(cJSON_GetObjectItem(root, "details"), "unknown error")); get_string(cJSON_GetObjectItem(root, "details"), "unknown error"));
cJSON_Delete(root); cJSON_Delete(root);
@@ -414,25 +444,55 @@ static esp_err_t scryfall_do_fetch(const char *card_name, const char *uid)
ret = ESP_ERR_NO_MEM; ret = ESP_ERR_NO_MEM;
} }
/* 4. Download cover art (best effort) */ /* Signal metadata ready immediately so UI can display card text and stats */
if (signal_ui) {
snprintf(s_last_uid, sizeof(s_last_uid), "%s", uid);
s_result_bits = (ret == ESP_OK) ? EVT_CARD_CACHED : EVT_CARD_FAILED;
}
if (ret != ESP_OK) {
cJSON_Delete(root);
return ret;
}
/* 4. Download cover art in background (best effort) */
if (image_url && strlen(image_url) > 0) { if (image_url && strlen(image_url) > 0) {
char jpg_path[64]; char jpg_path[64];
snprintf(jpg_path, sizeof(jpg_path), STORAGE_CARDS_DIR "/%s.jpg", uid); snprintf(jpg_path, sizeof(jpg_path), STORAGE_CARDS_DIR "/%s.jpg", uid);
char enc_img[512]; char enc_img[512];
url_encode(image_url, enc_img, sizeof(enc_img)); url_encode(image_url, enc_img, sizeof(enc_img));
char proxy_url[600]; char proxy_url[600];
/* wsrv.nl converts Scryfall's progressive JPEG to a lightweight baseline JPEG preserving original aspect ratio */ /* wsrv.nl converts Scryfall's progressive JPEG to lightweight baseline JPEG (q=65 for 2-3x faster download & smaller flash footprint) */
snprintf(proxy_url, sizeof(proxy_url), "https://wsrv.nl/?url=%s&w=200&h=150&fit=inside&output=jpg", enc_img); snprintf(proxy_url, sizeof(proxy_url), "https://wsrv.nl/?url=%s&w=200&h=150&fit=inside&output=jpg&q=65", enc_img);
if (signal_ui) {
snprintf(s_fetching_img_uid, sizeof(s_fetching_img_uid), "%s", uid);
s_image_fetching = true;
}
esp_err_t dl = http_download_file(proxy_url, jpg_path); esp_err_t dl = http_download_file(proxy_url, jpg_path);
if (dl != ESP_OK) { if (dl != ESP_OK) {
ESP_LOGW(TAG, "proxied image download failed, trying direct: %s", image_url); ESP_LOGW(TAG, "proxied image download failed, trying direct: %s", image_url);
dl = http_download_file(image_url, jpg_path); dl = http_download_file(image_url, jpg_path);
} }
if (signal_ui) {
s_image_fetching = false;
snprintf(s_last_img_uid, sizeof(s_last_img_uid), "%s", uid);
s_image_bits = (dl == ESP_OK) ? EVT_IMG_CACHED : EVT_IMG_FAILED;
}
if (dl != ESP_OK) { if (dl != ESP_OK) {
ESP_LOGW(TAG, "image download failed: %s", image_url); ESP_LOGW(TAG, "image download failed: %s", image_url);
} else { } else {
ESP_LOGI(TAG, "image cached to %s", jpg_path); ESP_LOGI(TAG, "image cached to %s", jpg_path);
} }
} else {
if (signal_ui) {
s_image_fetching = false;
snprintf(s_last_img_uid, sizeof(s_last_img_uid), "%s", uid);
s_image_bits = EVT_IMG_FAILED;
}
} }
cJSON_Delete(root); cJSON_Delete(root);
@@ -445,10 +505,7 @@ static esp_err_t scryfall_do_fetch(const char *card_name, const char *uid)
static esp_err_t scryfall_ensure_cached(const char *card_name, const char *uid) static esp_err_t scryfall_ensure_cached(const char *card_name, const char *uid)
{ {
esp_err_t ret = scryfall_do_fetch(card_name, uid); return scryfall_do_fetch(card_name, uid, true);
snprintf(s_last_uid, sizeof(s_last_uid), "%s", uid);
s_result_bits = (ret == ESP_OK) ? EVT_CARD_CACHED : EVT_CARD_FAILED;
return ret;
} }
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@@ -486,8 +543,8 @@ static void prefetch_mappings(void)
cJSON_ArrayForEach(item, root) { cJSON_ArrayForEach(item, root) {
uid = item->string; uid = item->string;
if (uid == NULL || item->valuestring == NULL) continue; if (uid == NULL || item->valuestring == NULL) continue;
/* Call scryfall_do_fetch directly so we do not clobber UI fetch signaling */ /* Call scryfall_do_fetch directly with signal_ui=false so we do not clobber UI fetch signaling */
scryfall_do_fetch(item->valuestring, uid); scryfall_do_fetch(item->valuestring, uid, false);
/* Rate-limit delay: Scryfall requests 50-100ms between queries */ /* Rate-limit delay: Scryfall requests 50-100ms between queries */
vTaskDelay(pdMS_TO_TICKS(150)); vTaskDelay(pdMS_TO_TICKS(150));
} }
@@ -567,4 +624,35 @@ const char *scryfall_last_result_uid(void)
void scryfall_consume_result(void) void scryfall_consume_result(void)
{ {
s_result_bits = 0; s_result_bits = 0;
}
bool scryfall_has_pending_image(void)
{
return (s_image_bits & (EVT_IMG_CACHED | EVT_IMG_FAILED)) != 0;
}
bool scryfall_last_image_success(void)
{
return (s_image_bits & EVT_IMG_CACHED) != 0;
}
const char *scryfall_last_image_uid(void)
{
return s_last_img_uid;
}
void scryfall_consume_image(void)
{
s_image_bits = 0;
}
bool scryfall_is_fetching_image(const char *uid)
{
if (!s_image_fetching) {
return false;
}
if (uid == NULL || uid[0] == '\0') {
return false;
}
return (strcasecmp(s_fetching_img_uid, uid) == 0);
} }

View File

@@ -50,6 +50,31 @@ const char *scryfall_last_result_uid(void);
*/ */
void scryfall_consume_result(void); void scryfall_consume_result(void);
/**
* @brief True if net_task finished an image fetch attempt.
*/
bool scryfall_has_pending_image(void);
/**
* @brief True if the last image fetch succeeded.
*/
bool scryfall_last_image_success(void);
/**
* @brief UID of the most recently fetched card image.
*/
const char *scryfall_last_image_uid(void);
/**
* @brief Clear the pending image result flag.
*/
void scryfall_consume_image(void);
/**
* @brief True if an image download is actively in progress for this UID.
*/
bool scryfall_is_fetching_image(const char *uid);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@@ -1616,6 +1616,11 @@ static void view_card(void)
bool has_art = load_art_crop(s_uid); bool has_art = load_art_crop(s_uid);
lv_coord_t prev_scroll = 0;
if (s_card_scroll_cont != NULL) {
prev_scroll = lv_obj_get_scroll_y(s_card_scroll_cont);
}
lvgl_port_lock(0); lvgl_port_lock(0);
ui_clean_screen(); ui_clean_screen();
@@ -1698,6 +1703,25 @@ static void view_card(void)
lv_obj_set_style_border_width(img, 1, 0); lv_obj_set_style_border_width(img, 1, 0);
lv_obj_set_style_radius(img, 3, 0); lv_obj_set_style_radius(img, 3, 0);
cur_y += s_art_img_dsc.header.h + 4; cur_y += s_art_img_dsc.header.h + 4;
} else if (scryfall_is_fetching_image(s_uid)) {
/* Compact collapsible banner while art is downloading in background */
lv_obj_t *banner = lv_obj_create(s_card_scroll_cont);
lv_obj_set_size(banner, DISP_W - 32, 28);
lv_obj_align(banner, LV_ALIGN_TOP_MID, 0, cur_y);
lv_obj_set_style_bg_color(banner, lv_color_hex(0x222222), 0);
lv_obj_set_style_border_color(banner, lv_color_hex(COLOR_GOLD), 0);
lv_obj_set_style_border_width(banner, 1, 0);
lv_obj_set_style_radius(banner, 4, 0);
lv_obj_set_style_pad_all(banner, 2, 0);
lv_obj_clear_flag(banner, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t *blbl = lv_label_create(banner);
lv_label_set_text(blbl, "DOWNLOADING ART...");
lv_obj_set_style_text_font(blbl, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(blbl, lv_color_hex(COLOR_AMBER), 0);
lv_obj_align(blbl, LV_ALIGN_CENTER, 0, 0);
cur_y += 32;
} }
/* Type */ /* Type */
@@ -1722,6 +1746,10 @@ static void view_card(void)
lv_obj_align(oracle_lbl, LV_ALIGN_TOP_LEFT, 0, cur_y); lv_obj_align(oracle_lbl, LV_ALIGN_TOP_LEFT, 0, cur_y);
} }
if (prev_scroll > 0) {
lv_obj_scroll_to_y(s_card_scroll_cont, prev_scroll, LV_ANIM_OFF);
}
lvgl_port_unlock(); lvgl_port_unlock();
cJSON_Delete(root); cJSON_Delete(root);
} }
@@ -1920,6 +1948,18 @@ static void handle_fetch_ready(void)
} }
} }
static void handle_image_ready(void)
{
const char *img_uid = scryfall_last_image_uid();
scryfall_consume_image();
/* If the user is currently viewing this exact card, refresh view_card so art replaces the banner */
if (app_get_view() == APP_STATE_CARD_VIEW && img_uid != NULL && strcmp(img_uid, s_uid) == 0) {
ESP_LOGI(TAG, "Background art arrived for %s, updating card view", img_uid);
view_card();
}
}
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Encoder event dispatcher */ /* Encoder event dispatcher */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@@ -2283,6 +2323,11 @@ static void ui_task(void *arg)
handle_fetch_ready(); handle_fetch_ready();
} }
/* 2b. Async Scryfall card image ready */
if (scryfall_has_pending_image()) {
handle_image_ready();
}
/* 3. Button press & hold detection */ /* 3. Button press & hold detection */
int sw_level = gpio_get_level(HW_ENC_SW_GPIO); int sw_level = gpio_get_level(HW_ENC_SW_GPIO);
if (sw_level == 0) { if (sw_level == 0) {