diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 236a1ea..eec1560 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,6 +1,5 @@ idf_component_register(SRCS "main.c" - "spi_bus_manager.c" "storage_manager.c" "rfid_manager.c" "display_manager.c" diff --git a/main/app_state.c b/main/app_state.c index 6cc6586..5453be1 100644 --- a/main/app_state.c +++ b/main/app_state.c @@ -1,97 +1,40 @@ /* - * MTG RFID Companion - System state machine + * MTG RFID Companion - System state machine (ponytail: atomic scalar state) */ -#include -#include "freertos/FreeRTOS.h" -#include "freertos/semphr.h" #include "app_state.h" -static SemaphoreHandle_t s_lock; -static app_state_t s_view = APP_STATE_SCANNER; -static int32_t s_life = 20; -static int32_t s_life_preset = 20; - -const char *app_state_name(app_state_t state) -{ - switch (state) { - case APP_STATE_BOOT: return "BOOT"; - case APP_STATE_MENU: return "MENU"; - case APP_STATE_SCANNER: return "SCANNER"; - case APP_STATE_CARD_VIEW: return "CARD_VIEW"; - case APP_STATE_LIFE_COUNTER: return "LIFE_COUNTER"; - case APP_STATE_DICE_ROLLER: return "DICE_ROLLER"; - case APP_STATE_COMMANDER_TRACKER: return "COMMANDER_TRACKER"; - case APP_STATE_TOKEN_SPAWNER: return "TOKEN_SPAWNER"; - case APP_STATE_STORAGE_MANAGER: return "STORAGE_MANAGER"; - case APP_STATE_DEVICE_INFO: return "DEVICE_INFO"; - case APP_STATE_REGISTRATION: return "REGISTRATION"; - } - return "UNKNOWN"; -} - -static void ensure_lock(void) -{ - if (s_lock == NULL) { - s_lock = xSemaphoreCreateMutex(); - } -} +static volatile app_state_t s_view = APP_STATE_SCANNER; +static volatile int32_t s_life = 20; +static volatile int32_t s_life_preset = 20; void app_set_life(int32_t life) { - ensure_lock(); - xSemaphoreTake(s_lock, portMAX_DELAY); s_life = life; - xSemaphoreGive(s_lock); } int32_t app_get_life(void) { - ensure_lock(); - xSemaphoreTake(s_lock, portMAX_DELAY); - int32_t v = s_life; - xSemaphoreGive(s_lock); - return v; + return s_life; } void app_set_life_preset(int32_t preset) { - ensure_lock(); - xSemaphoreTake(s_lock, portMAX_DELAY); s_life_preset = preset; s_life = preset; - xSemaphoreGive(s_lock); } int32_t app_get_life_preset(void) { - ensure_lock(); - xSemaphoreTake(s_lock, portMAX_DELAY); - int32_t v = s_life_preset; - xSemaphoreGive(s_lock); - return v; + return s_life_preset; } void app_set_view(app_state_t view) { - ensure_lock(); - xSemaphoreTake(s_lock, portMAX_DELAY); s_view = view; - xSemaphoreGive(s_lock); } app_state_t app_get_view(void) { - ensure_lock(); - xSemaphoreTake(s_lock, portMAX_DELAY); - app_state_t v = s_view; - xSemaphoreGive(s_lock); - return v; -} - -void app_enter_keyed_view(app_state_t view) -{ - if (view == APP_STATE_LIFE_COUNTER || view == APP_STATE_CARD_VIEW) { - app_set_view(view); - } + return s_view; } \ No newline at end of file diff --git a/main/app_state.h b/main/app_state.h index ae11776..ac2f4b1 100644 --- a/main/app_state.h +++ b/main/app_state.h @@ -28,18 +28,15 @@ typedef enum { APP_STATE_REGISTRATION, } app_state_t; -const char *app_state_name(app_state_t state); - /* Life counter state */ void app_set_life(int32_t life); int32_t app_get_life(void); void app_set_life_preset(int32_t preset); int32_t app_get_life_preset(void); -/* Which "screen" is active - CARD_VIEW vs LIFE_COUNTER. */ +/* Current screen view */ void app_set_view(app_state_t view); app_state_t app_get_view(void); -void app_enter_keyed_view(app_state_t view); #ifdef __cplusplus } diff --git a/main/buzzer.c b/main/buzzer.c index f138717..56674ab 100644 --- a/main/buzzer.c +++ b/main/buzzer.c @@ -18,6 +18,7 @@ static const char *TAG = "buzzer"; #define BUZZER_CHANNEL LEDC_CHANNEL_0 static QueueHandle_t s_sound_queue = NULL; +static volatile bool s_melody_playing = false; static const buzzer_note_t MELODY_VICTORY[] = { {NOTE_C5, 90}, {NOTE_C5, 90}, {NOTE_C5, 90}, {NOTE_C5, 220}, @@ -57,6 +58,7 @@ static void buzzer_task(void *arg) buzzer_sound_t sound; while (1) { if (xQueueReceive(s_sound_queue, &sound, portMAX_DELAY) == pdTRUE) { + s_melody_playing = true; switch (sound) { case BUZZER_SOUND_VICTORY: buzzer_play_notes(MELODY_VICTORY, sizeof(MELODY_VICTORY) / sizeof(MELODY_VICTORY[0])); @@ -70,6 +72,7 @@ static void buzzer_task(void *arg) default: break; } + s_melody_playing = false; } } } @@ -139,6 +142,9 @@ esp_err_t buzzer_stop(void) esp_err_t buzzer_chirp(uint32_t freq_hz, uint32_t duration_ms) { + if (s_melody_playing) { + return ESP_OK; /* Don't disrupt ongoing background chiptune melody */ + } esp_err_t ret = buzzer_start(freq_hz, 30); if (ret != ESP_OK) { return ret; diff --git a/main/display_manager.c b/main/display_manager.c index a821be3..208e261 100644 --- a/main/display_manager.c +++ b/main/display_manager.c @@ -1,6 +1,3 @@ -/* - * MTG RFID Companion - ST7789 display manager with LVGL (ponytail: native library) - */ #include "esp_log.h" #include "esp_lcd_panel_io.h" @@ -27,6 +24,21 @@ esp_err_t display_manager_init(void) return ESP_OK; } + /* ponytail: native SPI bus init directly in display manager */ + spi_bus_config_t buscfg = { + .sclk_io_num = HW_SPI_SCK_GPIO, + .mosi_io_num = HW_SPI_MOSI_GPIO, + .miso_io_num = HW_SPI_MISO_GPIO, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + .max_transfer_sz = 240 * 80 * sizeof(uint16_t), + }; + esp_err_t ret = spi_bus_initialize(LCD_HOST, &buscfg, SPI_DMA_CH_AUTO); + if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) { + ESP_LOGE(TAG, "spi_bus_initialize failed: %s", esp_err_to_name(ret)); + return ret; + } + /* 1. Initialize ST7789 panel via esp_lcd */ esp_lcd_panel_io_handle_t io = NULL; esp_lcd_panel_io_spi_config_t io_config = { @@ -38,7 +50,7 @@ esp_err_t display_manager_init(void) .spi_mode = 0, .trans_queue_depth = 10, }; - esp_err_t ret = esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_HOST, &io_config, &io); + ret = esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_HOST, &io_config, &io); if (ret != ESP_OK) { ESP_LOGE(TAG, "panel io init failed: %s", esp_err_to_name(ret)); return ret; diff --git a/main/display_manager.h b/main/display_manager.h index b3f2d0e..435c40c 100644 --- a/main/display_manager.h +++ b/main/display_manager.h @@ -1,5 +1,4 @@ /* - * MTG RFID Companion - ST7789 display manager with LVGL (ponytail: native library) */ #pragma once diff --git a/main/encoder.c b/main/encoder.c index ed5ae56..4671775 100644 --- a/main/encoder.c +++ b/main/encoder.c @@ -22,11 +22,6 @@ static volatile int8_t s_accum = 0; static volatile uint8_t s_prev_state = 0x03; static volatile int8_t s_steps_per_detent = 4; /* 4 = full-step, 2 = half-step */ -static volatile bool s_button_pressed = false; -static volatile uint32_t s_last_sw_tick = 0; - -#define SW_DEBOUNCE_MS (35) - /* * Quadrature Gray-Code Transition Table * Index: (old_state << 2) | new_state (4-bit, 0..15) @@ -68,16 +63,7 @@ static const int8_t s_quad_table[16] = { static void IRAM_ATTR encoder_isr(void *arg) { - uint32_t gpio = (uint32_t)(uintptr_t)arg; - - if (gpio == HW_ENC_SW_GPIO) { - unsigned int now = (unsigned int)xTaskGetTickCountFromISR(); - if ((now - s_last_sw_tick) > pdMS_TO_TICKS(SW_DEBOUNCE_MS)) { - s_last_sw_tick = now; - s_button_pressed = true; - } - return; - } + (void)arg; /* Read current levels of both encoder channels */ uint8_t clk = (uint8_t)gpio_get_level(HW_ENC_CLK_GPIO); @@ -123,7 +109,7 @@ esp_err_t encoder_init(void) ESP_ERROR_CHECK(config_pin(HW_ENC_CLK_GPIO, GPIO_INTR_ANYEDGE)); ESP_ERROR_CHECK(config_pin(HW_ENC_DT_GPIO, GPIO_INTR_ANYEDGE)); - ESP_ERROR_CHECK(config_pin(HW_ENC_SW_GPIO, GPIO_INTR_NEGEDGE)); + ESP_ERROR_CHECK(config_pin(HW_ENC_SW_GPIO, GPIO_INTR_DISABLE)); /* Initialize initial state */ uint8_t clk = (uint8_t)gpio_get_level(HW_ENC_CLK_GPIO); @@ -132,9 +118,8 @@ esp_err_t encoder_init(void) s_accum = 0; s_delta = 0; - ESP_ERROR_CHECK(gpio_isr_handler_add(HW_ENC_CLK_GPIO, encoder_isr, (void *)(uintptr_t)HW_ENC_CLK_GPIO)); - ESP_ERROR_CHECK(gpio_isr_handler_add(HW_ENC_DT_GPIO, encoder_isr, (void *)(uintptr_t)HW_ENC_DT_GPIO)); - ESP_ERROR_CHECK(gpio_isr_handler_add(HW_ENC_SW_GPIO, encoder_isr, (void *)(uintptr_t)HW_ENC_SW_GPIO)); + ESP_ERROR_CHECK(gpio_isr_handler_add(HW_ENC_CLK_GPIO, encoder_isr, NULL)); + ESP_ERROR_CHECK(gpio_isr_handler_add(HW_ENC_DT_GPIO, encoder_isr, NULL)); ESP_LOGI(TAG, "EC11 quadrature encoder ready (CLK=%d DT=%d SW=%d steps/tick=%d)", HW_ENC_CLK_GPIO, HW_ENC_DT_GPIO, HW_ENC_SW_GPIO, s_steps_per_detent); @@ -161,24 +146,9 @@ bool encoder_has_delta(void) return s_delta != 0; } -int32_t encoder_get_delta(void) -{ - return (int32_t)s_delta; -} - int32_t encoder_get_delta_and_reset(void) { int32_t v = (int32_t)s_delta; s_delta = 0; return v; -} - -bool encoder_button_pressed(void) -{ - return s_button_pressed; -} - -void encoder_clear_button(void) -{ - s_button_pressed = false; } \ No newline at end of file diff --git a/main/encoder.h b/main/encoder.h index 7d0406f..a6603fe 100644 --- a/main/encoder.h +++ b/main/encoder.h @@ -31,21 +31,6 @@ bool encoder_has_delta(void); */ int32_t encoder_get_delta_and_reset(void); -/** - * @brief Return current accumulator without resetting. - */ -int32_t encoder_get_delta(void); - -/** - * @brief True if the push button was pressed since last poll. - */ -bool encoder_button_pressed(void); - -/** - * @brief Clear the button-pressed flag. - */ -void encoder_clear_button(void); - /** * @brief Get current quadrature steps per mechanical detent tick (2 or 4). */ diff --git a/main/main.c b/main/main.c index 1ba92fb..a87a550 100644 --- a/main/main.c +++ b/main/main.c @@ -2,7 +2,6 @@ #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_log.h" -#include "spi_bus_manager.h" #include "storage_manager.h" #include "rfid_manager.h" #include "display_manager.h" @@ -17,7 +16,6 @@ void app_main(void) { ESP_LOGI(TAG, "MTG RFID Companion booting..."); - ESP_ERROR_CHECK(spi_bus_manager_init()); ESP_ERROR_CHECK(storage_manager_init()); /* RFID comes BEFORE the display: the reader lives on its own SPI3 bus @@ -49,9 +47,6 @@ void app_main(void) ESP_ERROR_CHECK(ui_task_start()); - ESP_LOGI(TAG, "Boot sequence complete."); - - while (1) { - vTaskDelay(pdMS_TO_TICKS(1000)); - } + ESP_LOGI(TAG, "Boot sequence complete. Main task exiting to reclaim stack."); + vTaskDelete(NULL); } \ No newline at end of file diff --git a/main/rfid_manager.c b/main/rfid_manager.c index b8829a9..26a29db 100644 --- a/main/rfid_manager.c +++ b/main/rfid_manager.c @@ -11,7 +11,6 @@ #include "rc522_picc.h" #include "driver/rc522_spi.h" #include "hw_pins.h" -#include "spi_bus_manager.h" #include "rfid_manager.h" static const char *TAG = "rfid"; @@ -72,12 +71,6 @@ static void on_picc_state_changed(void *arg, esp_event_base_t base, int32_t even } } -/* Read the MFRC522 VersionReg (0x37) directly over SPI3 with a temporary - * hardware-CS device. Any plausible value (0x88/0x91/0x92) means the chip is - * alive and MISO works; 0x00/0xFF means MISO/power/wiring is dead. This is a - * pure diagnostic and does not depend on the rc522 driver's reset path. */ - - esp_err_t rfid_manager_init(void) { esp_log_level_set("rc522", ESP_LOG_INFO); @@ -166,8 +159,10 @@ esp_err_t rfid_manager_start(void) #else if (ret != ESP_OK) { ESP_LOGE(TAG, "MFRC522 self-test failed (%s). Check: reader powered " - "(3.3V), SPI wiring (SCK=16/MOSI=17/MISO=13, CS=26, RST=22), " - "and if it is a clone try lowering RC522_SPI_CLK_HZ", esp_err_to_name(ret)); + "(3.3V), SPI wiring (SCK=%d/MOSI=%d/MISO=%d, CS=%d, RST=%d), " + "and if it is a clone try lowering RC522_SPI_CLK_HZ", + esp_err_to_name(ret), + HW_RFID_SCK_GPIO, HW_RFID_MOSI_GPIO, HW_RFID_MISO_GPIO, HW_RC522_CS_GPIO, HW_RC522_RST_GPIO); return ret; } #endif @@ -182,7 +177,7 @@ esp_err_t rfid_manager_start(void) ESP_LOGW(TAG, "%s handle not found, affinity not set", RC522_TASK_NAME); } - ESP_LOGI(TAG, "RFID scanner ready (SPI3_HOST, CS=%d)RST=%d)", + ESP_LOGI(TAG, "RFID scanner ready (SPI3_HOST, CS=%d RST=%d)", HW_RC522_CS_GPIO, HW_RC522_RST_GPIO); return ESP_OK; } diff --git a/main/scryfall_client.c b/main/scryfall_client.c index 0d2280e..85ed7d6 100644 --- a/main/scryfall_client.c +++ b/main/scryfall_client.c @@ -36,6 +36,7 @@ static const char *TAG = "scryfall"; #define IMAGE_URL_MAX (512) #define EVT_CARD_CACHED (1U << 0) +#define EVT_CARD_FAILED (1U << 1) typedef struct { char name[CARD_NAME_MAX]; @@ -45,7 +46,7 @@ typedef struct { static QueueHandle_t s_fetch_queue; static TaskHandle_t s_net_task; -static uint32_t s_result_bits = 0; +static volatile uint32_t s_result_bits = 0; static char s_last_uid[UID_MAX] = {0}; /* ------------------------------------------------------------------ */ @@ -98,9 +99,9 @@ static esp_err_t http_on_body(http_sink_t *sink, const char *data, size_t len) return ESP_OK; } - if (len > sink->cap - sink->len) { - /* grow, never beyond cap */ - size_t req = sink->len + len; + /* Reserve space for data plus 1 byte for null terminator */ + if (len + 1 > sink->cap - sink->len) { + size_t req = sink->len + len + 1; if (req > JSON_BUF_MAX) { sink->err = ESP_ERR_NO_MEM; return ESP_FAIL; @@ -148,6 +149,14 @@ static esp_err_t http_event_handler(esp_http_client_event_t *evt) static esp_err_t http_request(const char *url, http_sink_t *sink) { for (int attempt = 0; attempt < MAX_ATTEMPTS; attempt++) { + if (sink->file != NULL) { + rewind(sink->file); + ftruncate(fileno(sink->file), 0); + } else { + sink->len = 0; + } + sink->err = ESP_OK; + esp_http_client_config_t cfg = { .url = url, .transport_type = HTTP_TRANSPORT_OVER_SSL, @@ -230,7 +239,7 @@ static const char *get_string(const cJSON *item, const char *fallback) /* Cache metadata */ /* ------------------------------------------------------------------ */ -esp_err_t scryfall_ensure_cached(const char *card_name, const char *uid) +static esp_err_t scryfall_do_fetch(const char *card_name, const char *uid) { char json_path[64]; snprintf(json_path, sizeof(json_path), STORAGE_CARDS_DIR "/%s.json", uid); @@ -261,8 +270,6 @@ esp_err_t scryfall_ensure_cached(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); - s_result_bits |= EVT_CARD_CACHED; - snprintf(s_last_uid, sizeof(s_last_uid), "%s", uid); return ESP_OK; } ESP_LOGI(TAG, "json valid but %s missing, fetching art...", jpg_path); @@ -397,13 +404,19 @@ esp_err_t scryfall_ensure_cached(const char *card_name, const char *uid) cJSON_Delete(root); if (ret == ESP_OK) { - s_result_bits |= EVT_CARD_CACHED; - snprintf(s_last_uid, sizeof(s_last_uid), "%s", uid); ESP_LOGI(TAG, "card '%s' cached under %s", card_name, uid); } return ret; } +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; +} + /* ------------------------------------------------------------------ */ /* net_task */ /* ------------------------------------------------------------------ */ @@ -431,6 +444,8 @@ static void prefetch_mappings(void) uid = item->string; if (uid == NULL || item->valuestring == NULL) continue; scryfall_ensure_cached(item->valuestring, uid); + /* Rate-limit delay: Scryfall requests 50-100ms between queries */ + vTaskDelay(pdMS_TO_TICKS(100)); } cJSON_Delete(root); } @@ -490,6 +505,11 @@ esp_err_t scryfall_fetch_request(const char *card_name, const char *uid) } bool scryfall_has_pending_result(void) +{ + return (s_result_bits & (EVT_CARD_CACHED | EVT_CARD_FAILED)) != 0; +} + +bool scryfall_last_result_success(void) { return (s_result_bits & EVT_CARD_CACHED) != 0; } @@ -501,5 +521,5 @@ const char *scryfall_last_result_uid(void) void scryfall_consume_result(void) { - s_result_bits &= ~EVT_CARD_CACHED; + s_result_bits = 0; } \ No newline at end of file diff --git a/main/scryfall_client.h b/main/scryfall_client.h index 7aa7813..f50bdba 100644 --- a/main/scryfall_client.h +++ b/main/scryfall_client.h @@ -31,18 +31,17 @@ esp_err_t scryfall_client_start(void); esp_err_t scryfall_fetch_request(const char *card_name, const char *uid); /** - * @brief Synchronously ensure the card cache exists for uid; fetch if missing. - * Safe to call from net_task only. - */ -esp_err_t scryfall_ensure_cached(const char *card_name, const char *uid); - -/** - * @brief True if net_task finished caching a card since last consumption. + * @brief True if net_task finished a fetch attempt since last consumption. */ bool scryfall_has_pending_result(void); /** - * @brief UID of the most recently cached card. + * @brief True if the last fetch succeeded, false if it failed. + */ +bool scryfall_last_result_success(void); + +/** + * @brief UID of the most recently fetched card. */ const char *scryfall_last_result_uid(void); diff --git a/main/spi_bus_manager.c b/main/spi_bus_manager.c deleted file mode 100644 index dac121e..0000000 --- a/main/spi_bus_manager.c +++ /dev/null @@ -1,35 +0,0 @@ -/* - * MTG RFID Companion - Shared SPI bus manager - */ - -#include -#include "esp_log.h" -#include "spi_bus_manager.h" -#include "hw_pins.h" - -static const char *TAG = "spi_bus"; - -/* 240 x 80 rows x 2 bytes per pixel. Largest contiguous DMA transfer the - * display engine will issue. */ -#define HW_SPI_MAX_TRANSFER_SZ (240 * 80 * sizeof(uint16_t)) - -esp_err_t spi_bus_manager_init(void) -{ - spi_bus_config_t buscfg = { - .sclk_io_num = HW_SPI_SCK_GPIO, - .mosi_io_num = HW_SPI_MOSI_GPIO, - .miso_io_num = HW_SPI_MISO_GPIO, - .quadwp_io_num = -1, - .quadhd_io_num = -1, - .max_transfer_sz = HW_SPI_MAX_TRANSFER_SZ, - }; - - esp_err_t ret = spi_bus_initialize(HW_SPI_HOST, &buscfg, SPI_DMA_CH_AUTO); - if (ret != ESP_OK) { - ESP_LOGE(TAG, "spi_bus_initialize failed: %s", esp_err_to_name(ret)); - return ret; - } - ESP_LOGI(TAG, "SPI2_HOST ready: SCK=%d MOSI=%d MISO=%d", - HW_SPI_SCK_GPIO, HW_SPI_MOSI_GPIO, HW_SPI_MISO_GPIO); - return ESP_OK; -} \ No newline at end of file diff --git a/main/spi_bus_manager.h b/main/spi_bus_manager.h deleted file mode 100644 index ab3078b..0000000 --- a/main/spi_bus_manager.h +++ /dev/null @@ -1,27 +0,0 @@ -/* - * MTG RFID Companion - Shared SPI bus manager - * - * Phase 2 Step 2.3: Initializes the single SPI2_HOST (VSPI) bus shared by - * the ST7789 display and the MFRC522 RFID reader. - */ - -#pragma once - -#include "esp_err.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Initialize the shared SPI2_HOST bus. - * - * Must be called exactly once before the display and RFID driver attach - * their SPI devices. MFRC522 is passed a NULL bus_config so that it adds - * its device to this pre-existing bus instead of trying to own it. - */ -esp_err_t spi_bus_manager_init(void); - -#ifdef __cplusplus -} -#endif \ No newline at end of file diff --git a/main/storage_manager.c b/main/storage_manager.c index b989411..cbfafe2 100644 --- a/main/storage_manager.c +++ b/main/storage_manager.c @@ -16,8 +16,6 @@ static const char *TAG = "storage"; #define STORAGE_PARTITION_LABEL "storage" -#define STORAGE_BOOT_COUNT_FILE STORAGE_BASE_PATH "/boot_count.txt" -#define STORAGE_TEST_FILE STORAGE_BASE_PATH "/test.txt" static const char SAMPLE_MAPPINGS[] = "{\n" @@ -211,17 +209,6 @@ esp_err_t storage_manager_lookup_name(const char *uid, char **out) } cJSON *item = cJSON_GetObjectItem(root, uid); - if (item == NULL) { - char prefix[48]; - snprintf(prefix, sizeof(prefix), "%s&name=", uid); - cJSON *child = NULL; - cJSON_ArrayForEach(child, root) { - if (child->string && strncmp(child->string, prefix, strlen(prefix)) == 0) { - item = child; - break; - } - } - } esp_err_t ret = (item && cJSON_IsString(item) && item->valuestring) ? ESP_OK : ESP_ERR_NOT_FOUND; if (ret == ESP_OK) { *out = strdup(item->valuestring); @@ -258,38 +245,6 @@ static esp_err_t seed_defaults(void) return ESP_OK; } -static void cross_reboot_persistence_test(void) -{ - /* Persistent boot counter: proves writes survive a reset/reboot. */ - char buf[16] = {0}; - uint32_t count = 0; - - FILE *f = fopen(STORAGE_BOOT_COUNT_FILE, "rb"); - if (f != NULL) { - if (fread(buf, 1, sizeof(buf) - 1, f) > 0) { - count = (uint32_t)strtoul(buf, NULL, 10); - ESP_LOGI(TAG, "boot_count read back = %lu (persistence across reboots OK)", (unsigned long)count); - } - fclose(f); - } - - count++; - snprintf(buf, sizeof(buf), "%lu\n", (unsigned long)count); - storage_write_text(STORAGE_BOOT_COUNT_FILE, buf); - ESP_LOGI(TAG, "boot_count updated to %lu", (unsigned long)count); - - /* Sanity write/read within the same boot. */ - storage_write_text(STORAGE_TEST_FILE, "MTG-RFID-COMPANION persistence test\n"); - char *rd = NULL; - size_t rd_len = 0; - if (storage_read_text(STORAGE_TEST_FILE, &rd, &rd_len) == ESP_OK && rd != NULL) { - ESP_LOGI(TAG, "test.txt echoed back: %s", rd); - free(rd); - } else { - ESP_LOGE(TAG, "test.txt read-back failed"); - } -} - esp_err_t storage_manager_init(void) { esp_vfs_littlefs_conf_t conf = { @@ -319,7 +274,6 @@ esp_err_t storage_manager_init(void) return ret; } - cross_reboot_persistence_test(); return ESP_OK; } diff --git a/main/ui_task.c b/main/ui_task.c index c57dfeb..306b9fa 100644 --- a/main/ui_task.c +++ b/main/ui_task.c @@ -1,5 +1,6 @@ /* * MTG RFID Companion - UI task with LVGL + */ #include #include @@ -36,7 +37,6 @@ static char s_uid[RFID_UID_STR_MAX_LEN] = {0}; static char s_card_name[128] = {0}; static bool s_waiting_fetch = false; static uint32_t s_led_off_tick = 0; -static lv_obj_t *s_life_label = NULL; static lv_img_dsc_t s_art_img_dsc; static uint16_t *s_art_pixels = NULL; @@ -241,39 +241,44 @@ static const char *get_json_str(cJSON *root, const char *key, const char *fallba static void ui_clean_screen(void) { free_art_buffer(); - s_life_label = NULL; lv_obj_clean(lv_scr_act()); lv_obj_set_style_bg_color(lv_scr_act(), lv_color_black(), 0); lv_obj_set_style_bg_opa(lv_scr_act(), LV_OPA_COVER, 0); } +/* ponytail: single shared header builder across all screens */ +static void ui_draw_header(const char *title, const char *subtitle) +{ + lv_obj_t *bar = lv_obj_create(lv_scr_act()); + lv_obj_set_size(bar, DISP_W, 4); + lv_obj_set_style_bg_color(bar, lv_color_hex(COLOR_GOLD), 0); + lv_obj_set_style_border_width(bar, 0, 0); + lv_obj_align(bar, LV_ALIGN_TOP_MID, 0, 0); + + if (title != NULL) { + lv_obj_t *t = lv_label_create(lv_scr_act()); + lv_label_set_text(t, title); + lv_obj_set_style_text_font(t, &lv_font_montserrat_20, 0); + lv_obj_set_style_text_color(t, lv_color_hex(COLOR_GOLD), 0); + lv_obj_align(t, LV_ALIGN_TOP_MID, 0, (subtitle != NULL) ? 10 : 14); + } + + if (subtitle != NULL) { + lv_obj_t *sub = lv_label_create(lv_scr_act()); + lv_label_set_text(sub, subtitle); + lv_obj_set_style_text_font(sub, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(sub, lv_color_hex(COLOR_AMBER), 0); + lv_obj_align(sub, LV_ALIGN_TOP_MID, 0, 34); + } +} + /* 1. Main Menu */ static void view_menu(void) { encoder_set_steps_per_detent(4); lvgl_port_lock(0); ui_clean_screen(); - - /* Top yellow accent bar */ - lv_obj_t *bar = lv_obj_create(lv_scr_act()); - lv_obj_set_size(bar, DISP_W, 4); - lv_obj_set_style_bg_color(bar, lv_color_hex(0xFFCC00), 0); - lv_obj_set_style_border_width(bar, 0, 0); - lv_obj_align(bar, LV_ALIGN_TOP_MID, 0, 0); - - /* Title */ - lv_obj_t *title = lv_label_create(lv_scr_act()); - lv_label_set_text(title, "MTG COMPANION"); - lv_obj_set_style_text_font(title, &lv_font_montserrat_20, 0); - lv_obj_set_style_text_color(title, lv_color_hex(0xFFCC00), 0); - lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10); - - /* Subtitle */ - lv_obj_t *sub = lv_label_create(lv_scr_act()); - lv_label_set_text(sub, "- MAIN MENU -"); - lv_obj_set_style_text_font(sub, &lv_font_montserrat_14, 0); - lv_obj_set_style_text_color(sub, lv_color_hex(COLOR_AMBER), 0); - lv_obj_align(sub, LV_ALIGN_TOP_MID, 0, 34); + ui_draw_header("MTG COMPANION", "- MAIN MENU -"); /* 7 Menu Items */ int start_y = 52; @@ -407,33 +412,33 @@ static void view_life(void) } /* Main Big Number Display */ - s_life_label = lv_label_create(lv_scr_act()); - lv_obj_set_style_text_font(s_life_label, &lv_font_montserrat_48, 0); - lv_obj_align(s_life_label, LV_ALIGN_CENTER, 0, -20); + lv_obj_t *life_label = lv_label_create(lv_scr_act()); + lv_obj_set_style_text_font(life_label, &lv_font_montserrat_48, 0); + lv_obj_align(life_label, LV_ALIGN_CENTER, 0, -20); bool lethal = false; if (s_life_mode == LIFE_MODE_TOTAL) { int32_t life = app_get_life(); - lv_label_set_text_fmt(s_life_label, "%ld", (long)life); + lv_label_set_text_fmt(life_label, "%ld", (long)life); lv_color_t color = (life <= 0) ? lv_color_hex(0xFF1744) : (life <= 10) ? lv_color_hex(0xFF9100) : (life <= 15) ? lv_color_hex(0xFFCC00) : lv_color_hex(0x00E676); - lv_obj_set_style_text_color(s_life_label, color, 0); + lv_obj_set_style_text_color(life_label, color, 0); if (life <= 0) lethal = true; } else if (s_life_mode == LIFE_MODE_POISON) { - lv_label_set_text_fmt(s_life_label, "%ld / 10", (long)s_poison); + lv_label_set_text_fmt(life_label, "%ld / 10", (long)s_poison); lv_color_t color = (s_poison >= 10) ? lv_color_hex(0xFF1744) : (s_poison >= 6) ? lv_color_hex(0xFF9100) : lv_color_hex(0x00E5FF); - lv_obj_set_style_text_color(s_life_label, color, 0); + lv_obj_set_style_text_color(life_label, color, 0); if (s_poison >= 10) lethal = true; } else { - lv_label_set_text_fmt(s_life_label, "%ld / 21", (long)s_cmd_dmg); + lv_label_set_text_fmt(life_label, "%ld / 21", (long)s_cmd_dmg); lv_color_t color = (s_cmd_dmg >= 21) ? lv_color_hex(0xFF1744) : (s_cmd_dmg >= 15) ? lv_color_hex(0xFF9100) : lv_color_hex(0xE040FB); - lv_obj_set_style_text_color(s_life_label, color, 0); + lv_obj_set_style_text_color(life_label, color, 0); if (s_cmd_dmg >= 21) lethal = true; } @@ -481,12 +486,7 @@ static void view_dice(void) lvgl_port_lock(0); ui_clean_screen(); - /* Top yellow accent bar */ - lv_obj_t *bar = lv_obj_create(lv_scr_act()); - lv_obj_set_size(bar, DISP_W, 4); - lv_obj_set_style_bg_color(bar, lv_color_hex(0xFFCC00), 0); - lv_obj_set_style_border_width(bar, 0, 0); - lv_obj_align(bar, LV_ALIGN_TOP_MID, 0, 0); + ui_draw_header(NULL, NULL); /* Selector tabs */ const char *tabs[3] = {"[ D20 ]", "[ D6 ]", "[ COIN ]"}; @@ -630,18 +630,7 @@ static void view_commander_tax(void) lvgl_port_lock(0); ui_clean_screen(); - /* Top yellow accent bar */ - lv_obj_t *bar = lv_obj_create(lv_scr_act()); - lv_obj_set_size(bar, DISP_W, 4); - lv_obj_set_style_bg_color(bar, lv_color_hex(0xFFCC00), 0); - lv_obj_set_style_border_width(bar, 0, 0); - lv_obj_align(bar, LV_ALIGN_TOP_MID, 0, 0); - - lv_obj_t *title = lv_label_create(lv_scr_act()); - lv_label_set_text(title, "COMMANDER TAX"); - lv_obj_set_style_text_font(title, &lv_font_montserrat_20, 0); - lv_obj_set_style_text_color(title, lv_color_hex(0xFFCC00), 0); - lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 15); + ui_draw_header("COMMANDER TAX", NULL); /* Casts Box */ lv_obj_t *cast_box = lv_obj_create(lv_scr_act()); @@ -722,19 +711,7 @@ static void view_token_spawner(void) lvgl_port_lock(0); ui_clean_screen(); - /* Top yellow accent bar */ - lv_obj_t *bar = lv_obj_create(lv_scr_act()); - lv_obj_set_size(bar, DISP_W, 4); - lv_obj_set_style_bg_color(bar, lv_color_hex(0xFFCC00), 0); - lv_obj_set_style_border_width(bar, 0, 0); - lv_obj_align(bar, LV_ALIGN_TOP_MID, 0, 0); - - /* Title */ - lv_obj_t *title = lv_label_create(lv_scr_act()); - lv_label_set_text(title, "TOKEN SPAWNER"); - lv_obj_set_style_text_font(title, &lv_font_montserrat_20, 0); - lv_obj_set_style_text_color(title, lv_color_hex(0xFFCC00), 0); - lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10); + ui_draw_header("TOKEN SPAWNER", NULL); /* Carousel indicator: < 1 OF 8 > */ lv_obj_t *idx_lbl = lv_label_create(lv_scr_act()); @@ -843,18 +820,7 @@ static void view_storage(void) lvgl_port_lock(0); ui_clean_screen(); - /* Top yellow accent bar */ - lv_obj_t *bar = lv_obj_create(lv_scr_act()); - lv_obj_set_size(bar, DISP_W, 4); - lv_obj_set_style_bg_color(bar, lv_color_hex(0xFFCC00), 0); - lv_obj_set_style_border_width(bar, 0, 0); - lv_obj_align(bar, LV_ALIGN_TOP_MID, 0, 0); - - lv_obj_t *title = lv_label_create(lv_scr_act()); - lv_label_set_text(title, "DECK & STORAGE"); - lv_obj_set_style_text_font(title, &lv_font_montserrat_20, 0); - lv_obj_set_style_text_color(title, lv_color_hex(0xFFCC00), 0); - lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 12); + ui_draw_header("DECK & STORAGE", NULL); size_t total_b = 0, used_b = 0; storage_manager_get_info(&total_b, &used_b); @@ -1018,18 +984,7 @@ static void view_device_info(void) lvgl_port_lock(0); ui_clean_screen(); - /* Top yellow accent bar */ - lv_obj_t *bar = lv_obj_create(lv_scr_act()); - lv_obj_set_size(bar, DISP_W, 4); - lv_obj_set_style_bg_color(bar, lv_color_hex(0xFFCC00), 0); - lv_obj_set_style_border_width(bar, 0, 0); - lv_obj_align(bar, LV_ALIGN_TOP_MID, 0, 0); - - lv_obj_t *title = lv_label_create(lv_scr_act()); - lv_label_set_text(title, "DEVICE INFO"); - lv_obj_set_style_text_font(title, &lv_font_montserrat_20, 0); - lv_obj_set_style_text_color(title, lv_color_hex(0xFFCC00), 0); - lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 15); + ui_draw_header("DEVICE INFO", NULL); lv_obj_t *box = lv_obj_create(lv_scr_act()); lv_obj_set_size(box, DISP_W - 24, 220); @@ -1416,10 +1371,55 @@ static void handle_fetch_ready(void) { const char *ready_uid = scryfall_last_result_uid(); if (ready_uid != NULL && strcmp(ready_uid, s_uid) == 0) { + bool success = scryfall_last_result_success(); scryfall_consume_result(); s_waiting_fetch = false; - read_cached_color(s_uid); - view_card(); + if (success) { + read_cached_color(s_uid); + view_card(); + } else { + buzzer_chirp(220, 150); + led_manager_set_color(255, 0, 0); + s_led_off_tick = xTaskGetTickCount() + pdMS_TO_TICKS(LED_HOLD_MS); + + lvgl_port_lock(0); + ui_clean_screen(); + ui_draw_header("FETCH FAILED", NULL); + + lv_obj_t *box = lv_obj_create(lv_scr_act()); + lv_obj_set_size(box, DISP_W - 24, 180); + lv_obj_align(box, LV_ALIGN_CENTER, 0, -10); + lv_obj_set_style_bg_color(box, lv_color_hex(0x281010), 0); + lv_obj_set_style_border_color(box, lv_color_hex(0xFF1744), 0); + lv_obj_set_style_border_width(box, 2, 0); + lv_obj_set_style_radius(box, 8, 0); + lv_obj_set_style_pad_all(box, 8, 0); + + lv_obj_t *cname = lv_label_create(box); + lv_label_set_text(cname, s_card_name); + lv_obj_set_style_text_font(cname, &lv_font_montserrat_20, 0); + lv_obj_set_style_text_color(cname, lv_color_hex(0xFFCC00), 0); + lv_label_set_long_mode(cname, LV_LABEL_LONG_DOT); + lv_obj_set_width(cname, DISP_W - 44); + lv_obj_align(cname, LV_ALIGN_TOP_MID, 0, 10); + + lv_obj_t *sub = lv_label_create(box); + lv_label_set_text(sub, "Could not fetch card from\nScryfall. Check Wi-Fi STA\nor card spelling online.\n\nCLICK: RETRY"); + lv_obj_set_style_text_font(sub, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(sub, lv_color_white(), 0); + lv_obj_set_style_text_align(sub, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(sub, LV_ALIGN_CENTER, 0, 20); + + lv_obj_t *hint = lv_label_create(lv_scr_act()); + lv_label_set_text(hint, "HOLD: MAIN MENU"); + lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0); + lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8); + + lvgl_port_unlock(); + } + } else { + scryfall_consume_result(); } } @@ -1528,6 +1528,17 @@ static void handle_encoder_click(void) case APP_STATE_SCANNER: case APP_STATE_CARD_VIEW: case APP_STATE_REGISTRATION: + if (cur == APP_STATE_CARD_VIEW && s_card_name[0] != '\0' && !s_waiting_fetch) { + char jp[72]; + snprintf(jp, sizeof(jp), STORAGE_CARDS_DIR "/%s.json", s_uid); + if (!storage_file_exists(jp)) { + /* Retry fetch */ + s_waiting_fetch = true; + view_fetching(s_card_name); + scryfall_fetch_request(s_card_name, s_uid); + break; + } + } app_set_view(APP_STATE_LIFE_COUNTER); view_life(); break; @@ -1719,8 +1730,4 @@ esp_err_t ui_task_start(void) { xTaskCreatePinnedToCore(ui_task, "ui_task", UI_TASK_STACK, NULL, UI_TASK_PRIO, &s_ui_task, UI_CORE); return ESP_OK; -} - -void ui_task_wait_boot(void) -{ } \ No newline at end of file diff --git a/main/ui_task.h b/main/ui_task.h index 23cff8a..b770cc3 100644 --- a/main/ui_task.h +++ b/main/ui_task.h @@ -19,11 +19,6 @@ extern "C" { */ esp_err_t ui_task_start(void); -/** - * @brief Block until the Core-1 renderer task has finished its boot demo. - */ -void ui_task_wait_boot(void); - #ifdef __cplusplus } #endif \ No newline at end of file diff --git a/main/web_server.c b/main/web_server.c index e06ca15..0b6065a 100644 --- a/main/web_server.c +++ b/main/web_server.c @@ -154,42 +154,49 @@ static void send_json(httpd_req_t *req, int status, const char *body) static char *url_decode(const char *src) { - if (src == NULL) { - return NULL; - } - size_t len = strlen(src); - if (len > HTTPD_BODY_MAX - 1) { - len = HTTPD_BODY_MAX - 1; - } - char *out = malloc(len + 1); - if (out == NULL) { - return NULL; - } - size_t o = 0; - for (size_t i = 0; i < len && src[i] != '\0'; i++) { - if (src[i] == '%' && i + 2 < len) { - int hi = src[i + 1] >= 'a' ? src[i + 1] - 'a' + 10 - : src[i + 1] >= 'A' ? src[i + 1] - 'A' + 10 - : src[i + 1] - '0'; - int lo = src[i + 2] >= 'a' ? src[i + 2] - 'a' + 10 - : src[i + 2] >= 'A' ? src[i + 2] - 'A' + 10 - : src[i + 2] - '0'; - if (hi < 0 || hi > 15 || lo < 0 || lo > 15) { - free(out); - return NULL; - } - out[o++] = (char)((hi << 4) | lo); - i += 2; - } else if (src[i] == '+') { - out[o++] = ' '; + if (!src) return NULL; + char *out = strdup(src); + if (!out) return NULL; + char *dst = out; + for (const char *s = src; *s; s++) { + if (*s == '%' && s[1] && s[2]) { + char hex[3] = { s[1], s[2], '\0' }; + *dst++ = (char)strtol(hex, NULL, 16); + s += 2; + } else if (*s == '+') { + *dst++ = ' '; } else { - out[o++] = src[i]; + *dst++ = *s; } } - out[o] = '\0'; + *dst = '\0'; return out; } +static char *get_form_param(const char *form, const char *key) +{ + if (!form || !key) return NULL; + size_t klen = strlen(key); + const char *p = form; + while (p && *p) { + if (strncmp(p, key, klen) == 0 && p[klen] == '=') { + const char *val_start = p + klen + 1; + const char *val_end = strchr(val_start, '&'); + size_t vlen = val_end ? (size_t)(val_end - val_start) : strlen(val_start); + char *raw = malloc(vlen + 1); + if (!raw) return NULL; + memcpy(raw, val_start, vlen); + raw[vlen] = '\0'; + char *dec = url_decode(raw); + free(raw); + return dec; + } + p = strchr(p, '&'); + if (p) p++; + } + return NULL; +} + /* ------------------------------------------------------------------ */ /* Endpoints */ /* ------------------------------------------------------------------ */ @@ -234,22 +241,8 @@ static esp_err_t handle_bind(httpd_req_t *req) } form[got] = '\0'; - char *uid = NULL; - char *name = NULL; - char *u = strstr(form, "uid="); - char *n = strstr(form, "name="); - if (u) { - char *end = strchr(u, '&'); - if (end) *end = '\0'; - uid = url_decode(u + 4); - if (end) *end = '&'; - } - if (n) { - char *end = strchr(n, '&'); - if (end) *end = '\0'; - name = url_decode(n + 5); - if (end) *end = '&'; - } + char *uid = get_form_param(form, "uid"); + char *name = get_form_param(form, "name"); free(form); if (uid) { @@ -355,14 +348,7 @@ static esp_err_t handle_unbind(httpd_req_t *req) } form[got] = '\0'; - char *uid = NULL; - char *u = strstr(form, "uid="); - if (u) { - char *end = strchr(u, '&'); - if (end) *end = '\0'; - uid = url_decode(u + 4); - if (end) *end = '&'; - } + char *uid = get_form_param(form, "uid"); free(form); if (uid) { diff --git a/main/wifi_manager.c b/main/wifi_manager.c index b740dfe..fe3a37e 100644 --- a/main/wifi_manager.c +++ b/main/wifi_manager.c @@ -32,10 +32,9 @@ static void wifi_event_handler(void *arg, esp_event_base_t base, int32_t id, voi esp_wifi_connect(); } else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) { strncpy(s_sta_ip, "No Wi-Fi", sizeof(s_sta_ip)); + xEventGroupClearBits(s_wifi_group, WIFI_CONNECTED_BIT); if (s_retry_count < WIFI_RETRY_MAX) { - ESP_LOGW(TAG, "disconnected (%d), retrying in %d ms", s_retry_count, WIFI_RECONNECT_DELAY_MS); - xEventGroupClearBits(s_wifi_group, WIFI_CONNECTED_BIT); - vTaskDelay(pdMS_TO_TICKS(WIFI_RECONNECT_DELAY_MS)); + ESP_LOGW(TAG, "disconnected (%d), reconnecting...", s_retry_count); esp_wifi_connect(); s_retry_count++; } diff --git a/sdkconfig.defaults.esp32 b/sdkconfig.defaults.esp32 index 8315fe2..00c0ac2 100644 --- a/sdkconfig.defaults.esp32 +++ b/sdkconfig.defaults.esp32 @@ -11,7 +11,6 @@ CONFIG_MTG_WIFI_SSID="WiFimodem-FD58" CONFIG_MTG_WIFI_PASS="gzhrt5yzm3" -# LVGL Configuration (ponytail: native library settings) CONFIG_LV_COLOR_16_SWAP=y CONFIG_LV_FONT_MONTSERRAT_14=y CONFIG_LV_FONT_MONTSERRAT_20=y