From e6760e4866567eaa99ef8c240f8f3a33a1616ff8 Mon Sep 17 00:00:00 2001 From: Rasmus Date: Sun, 13 Sep 2026 23:31:36 +0200 Subject: [PATCH] faster image processing --- main/scryfall_client.c | 110 ++++++++++++++++++++++++++++++++++++----- main/scryfall_client.h | 25 ++++++++++ main/ui_task.c | 45 +++++++++++++++++ 3 files changed, 169 insertions(+), 11 deletions(-) diff --git a/main/scryfall_client.c b/main/scryfall_client.c index d72d1cd..3409008 100644 --- a/main/scryfall_client.c +++ b/main/scryfall_client.c @@ -37,6 +37,8 @@ static const char *TAG = "scryfall"; #define EVT_CARD_CACHED (1U << 0) #define EVT_CARD_FAILED (1U << 1) +#define EVT_IMG_CACHED (1U << 2) +#define EVT_IMG_FAILED (1U << 3) typedef struct { char name[CARD_NAME_MAX]; @@ -49,6 +51,11 @@ static TaskHandle_t s_net_task; static volatile uint32_t s_result_bits = 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 */ /* ------------------------------------------------------------------ */ @@ -181,7 +188,8 @@ static esp_err_t http_request(const char *url, http_sink_t *sink) .timeout_ms = HTTP_TIMEOUT_MS, .event_handler = http_event_handler, .user_data = sink, - .buffer_size = 2048, + .buffer_size = 4096, + .buffer_size_tx = 1024, .crt_bundle_attach = esp_crt_bundle_attach, .keep_alive_enable = false, }; @@ -271,7 +279,7 @@ static const char *get_string(const cJSON *item, const char *fallback) /* 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]; 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 (storage_file_exists(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; } 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 { ESP_LOGW(TAG, "cache entry %s is invalid or card mismatch (expected '%s'), re-fetching...", json_path, card_name); 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) { free(body); 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; } @@ -330,11 +352,19 @@ static esp_err_t scryfall_do_fetch(const char *card_name, const char *uid) free(body); if (root == NULL) { 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; } cJSON *obj = cJSON_GetObjectItem(root, "object"); 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", get_string(cJSON_GetObjectItem(root, "details"), "unknown error")); 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; } - /* 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) { char jpg_path[64]; snprintf(jpg_path, sizeof(jpg_path), STORAGE_CARDS_DIR "/%s.jpg", uid); char enc_img[512]; url_encode(image_url, enc_img, sizeof(enc_img)); char proxy_url[600]; - /* wsrv.nl converts Scryfall's progressive JPEG to a lightweight baseline JPEG preserving original aspect ratio */ - snprintf(proxy_url, sizeof(proxy_url), "https://wsrv.nl/?url=%s&w=200&h=150&fit=inside&output=jpg", enc_img); + /* 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&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); if (dl != ESP_OK) { ESP_LOGW(TAG, "proxied image download failed, trying direct: %s", image_url); 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) { ESP_LOGW(TAG, "image download failed: %s", image_url); } else { 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); @@ -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) { - esp_err_t ret = scryfall_do_fetch(card_name, uid); - snprintf(s_last_uid, sizeof(s_last_uid), "%s", uid); - s_result_bits = (ret == ESP_OK) ? EVT_CARD_CACHED : EVT_CARD_FAILED; - return ret; + return scryfall_do_fetch(card_name, uid, true); } /* ------------------------------------------------------------------ */ @@ -486,8 +543,8 @@ static void prefetch_mappings(void) cJSON_ArrayForEach(item, root) { uid = item->string; if (uid == NULL || item->valuestring == NULL) continue; - /* Call scryfall_do_fetch directly so we do not clobber UI fetch signaling */ - scryfall_do_fetch(item->valuestring, uid); + /* Call scryfall_do_fetch directly with signal_ui=false so we do not clobber UI fetch signaling */ + scryfall_do_fetch(item->valuestring, uid, false); /* Rate-limit delay: Scryfall requests 50-100ms between queries */ vTaskDelay(pdMS_TO_TICKS(150)); } @@ -567,4 +624,35 @@ const char *scryfall_last_result_uid(void) void scryfall_consume_result(void) { 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); } \ No newline at end of file diff --git a/main/scryfall_client.h b/main/scryfall_client.h index f50bdba..09bb690 100644 --- a/main/scryfall_client.h +++ b/main/scryfall_client.h @@ -50,6 +50,31 @@ const char *scryfall_last_result_uid(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 } #endif \ No newline at end of file diff --git a/main/ui_task.c b/main/ui_task.c index e9be628..3219e21 100644 --- a/main/ui_task.c +++ b/main/ui_task.c @@ -1616,6 +1616,11 @@ static void view_card(void) 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); 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_radius(img, 3, 0); 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 */ @@ -1722,6 +1746,10 @@ static void view_card(void) 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(); 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 */ /* ------------------------------------------------------------------ */ @@ -2283,6 +2323,11 @@ static void ui_task(void *arg) handle_fetch_ready(); } + /* 2b. Async Scryfall card image ready */ + if (scryfall_has_pending_image()) { + handle_image_ready(); + } + /* 3. Button press & hold detection */ int sw_level = gpio_get_level(HW_ENC_SW_GPIO); if (sw_level == 0) {