faster image processing
All checks were successful
ESP32 Build & Release / build (push) Successful in 1m16s
All checks were successful
ESP32 Build & Release / build (push) Successful in 1m16s
This commit is contained in:
@@ -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));
|
||||||
}
|
}
|
||||||
@@ -568,3 +625,34 @@ 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);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user