From e09ec049461aa341113503c1a14ab35a6edbcb0e Mon Sep 17 00:00:00 2001 From: Rasmus Date: Wed, 9 Sep 2026 17:35:54 +0200 Subject: [PATCH] Phase 4: ST7789 240x320 panel (esp_lcd), 5x7 font, Core 1 UI task, streaming tjpgd JPEG decode + baked storage image --- CMakeLists.txt | 5 +- main/CMakeLists.txt | 2 + main/display_manager.c | 237 ++++++++++++++++++++++++++++++++++++++ main/display_manager.h | 71 ++++++++++++ main/idf_component.yml | 3 +- main/main.c | 7 +- main/ui_font.h | 114 ++++++++++++++++++ main/ui_task.c | 75 ++++++++++++ main/ui_task.h | 29 +++++ sdkconfig | 17 +++ sdkconfig.defaults | 8 +- storage/cards/sample.jpg | Bin 0 -> 23530 bytes storage/cards/sample.json | 7 ++ storage/mappings.json | 4 + tools/make_sample_jpg.ps1 | 56 +++++++++ 15 files changed, 631 insertions(+), 4 deletions(-) create mode 100644 main/display_manager.c create mode 100644 main/display_manager.h create mode 100644 main/ui_font.h create mode 100644 main/ui_task.c create mode 100644 main/ui_task.h create mode 100644 storage/cards/sample.jpg create mode 100644 storage/cards/sample.json create mode 100644 storage/mappings.json create mode 100644 tools/make_sample_jpg.ps1 diff --git a/CMakeLists.txt b/CMakeLists.txt index 2515540..2d300b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,4 +4,7 @@ include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(mtg-rfid-companion) idf_build_set_property(PROJECT_DESCRIPTION "MTG RFID Companion") -idf_build_set_property(PROJECT_HOMEPAGE_URL "https://scryfall.com") \ No newline at end of file +idf_build_set_property(PROJECT_HOMEPAGE_URL "https://scryfall.com") + +# Bake the seeded LittleFS contents (from ./storage) into the flash image +littlefs_create_partition_image(storage storage FLASH_IN_PROJECT) \ No newline at end of file diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index e8ed6ae..a24be07 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -3,5 +3,7 @@ idf_component_register(SRCS "spi_bus_manager.c" "storage_manager.c" "rfid_manager.c" + "display_manager.c" + "ui_task.c" INCLUDE_DIRS "." ) \ No newline at end of file diff --git a/main/display_manager.c b/main/display_manager.c new file mode 100644 index 0000000..bd8703f --- /dev/null +++ b/main/display_manager.c @@ -0,0 +1,237 @@ +/* + * MTG RFID Companion - ST7789 display manager (Core 1) + */ + +#include +#include +#include +#include "esp_log.h" +#include "esp_lcd_panel_io.h" +#include "esp_lcd_panel_ops.h" +#include "esp_lcd_panel_vendor.h" +#include "esp_lcd_panel_st7789.h" +#include "driver/spi_master.h" +#include "tjpgd.h" +#include "hw_pins.h" +#include "spi_bus_manager.h" +#include "display_manager.h" +#include "ui_font.h" + +static const char *TAG = "display"; + +#define LCD_HOST HW_SPI_HOST +#define LCD_PIXEL_CLOCK_HZ (20 * 1000 * 1000) +#define LCD_CMD_BITS 8 +#define LCD_PARAM_BITS 8 +#define LCD_TRANS_QUEUE 10 + +#define JPEG_POOL_SIZE (10 * 1024) +#define JPEG_SBUF (512) /* must match CONFIG_JD_SZBUF */ + +static esp_lcd_panel_handle_t s_panel; +static uint16_t *s_staging = NULL; /* 240 px line staging buffer for JPEG */ + +static esp_err_t panel_io_init(esp_lcd_panel_io_handle_t *io) +{ + esp_lcd_panel_io_spi_config_t io_config = { + .dc_gpio_num = HW_LCD_DC_GPIO, + .cs_gpio_num = HW_LCD_CS_GPIO, + .pclk_hz = LCD_PIXEL_CLOCK_HZ, + .lcd_cmd_bits = LCD_CMD_BITS, + .lcd_param_bits = LCD_PARAM_BITS, + .spi_mode = 0, + .trans_queue_depth = LCD_TRANS_QUEUE, + }; + return esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_HOST, &io_config, io); +} + +esp_err_t display_manager_init(void) +{ + if (s_panel != NULL) { + return ESP_OK; + } + + esp_lcd_panel_io_handle_t io = NULL; + esp_err_t ret = panel_io_init(&io); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "panel io init failed: %s", esp_err_to_name(ret)); + return ret; + } + + esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = HW_LCD_RST_GPIO, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, + .bits_per_pixel = 16, + }; + ret = esp_lcd_new_panel_st7789(io, &panel_config, &s_panel); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "st7789 panel create failed: %s", esp_err_to_name(ret)); + return ret; + } + + ESP_ERROR_CHECK(esp_lcd_panel_reset(s_panel)); + ESP_ERROR_CHECK(esp_lcd_panel_init(s_panel)); + ESP_ERROR_CHECK(esp_lcd_panel_invert_color(s_panel, true)); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(s_panel, true)); + + s_staging = malloc(DISP_W * sizeof(uint16_t)); + if (s_staging == NULL) { + ESP_LOGE(TAG, "staging buffer alloc failed"); + return ESP_ERR_NO_MEM; + } + + ESP_LOGI(TAG, "ST7789 %dx%d ready (CS=%d DC=%d RST=%d)", + DISP_W, DISP_H, HW_LCD_CS_GPIO, HW_LCD_DC_GPIO, HW_LCD_RST_GPIO); + return ESP_OK; +} + +void display_draw_bitmap(int x0, int y0, int x1, int y1, const uint16_t *pixels) +{ + if (s_panel == NULL || pixels == NULL) { + return; + } + esp_lcd_panel_draw_bitmap(s_panel, x0, y0, x1, y1, pixels); +} + +void display_fill_rect(int x0, int y0, int x1, int y1, uint16_t color) +{ + if (x0 < 0) x0 = 0; + if (y0 < 0) y0 = 0; + if (x1 > DISP_W) x1 = DISP_W; + if (y1 > DISP_H) y1 = DISP_H; + if (x1 <= x0 || y1 <= y0) { + return; + } + + int width = x1 - x0; + int height = y1 - y0; + + static uint16_t line[DISP_W]; + for (int i = 0; i < width; i++) { + line[i] = color; + } + for (int r = 0; r < height; r++) { + display_draw_bitmap(x0, y0 + r, x1, y0 + r + 1, line); + } +} + +void display_fill(uint16_t color) +{ + display_fill_rect(0, 0, DISP_W, DISP_H, color); +} + +int display_put_text(int x, int y, const char *str, uint16_t fg, uint16_t bg) +{ + int x_start = x; + + while (str != NULL && *str != '\0') { + char c = *str++; + if (c >= FONT5X7_FIRST_CHAR && c <= FONT5X7_LAST_CHAR) { + const uint8_t *glyph = font5x7[c - FONT5X7_FIRST_CHAR]; + uint16_t cell[FONT5X7_W][FONT5X7_H]; + + for (int gy = 0; gy < FONT5X7_H; gy++) { + for (int gx = 0; gx < FONT5X7_W; gx++) { + cell[gx][gy] = (glyph[gy] >> gx) & 1 ? fg : bg; + } + } + if (x + FONT5X7_W <= DISP_W && y + FONT5X7_H <= DISP_H) { + display_draw_bitmap(x, y, x + FONT5X7_W, y + FONT5X7_H, &cell[0][0]); + } + x += FONT5X7_W + 1; + } else if (c == ' ') { + x += FONT5X7_W + 1; + } + } + return x - x_start; +} + +void display_put_text_center(int y, const char *str, uint16_t fg, uint16_t bg) +{ + int len = (int)strlen(str); + int width = len * (FONT5X7_W + 1) - 1; + int x = (DISP_W - width) / 2; + if (x < 0) x = 0; + display_put_text(x, y, str, fg, bg); +} + +/* ------------------------------------------------------------------ */ +/* tjpgd streaming decode */ +/* ------------------------------------------------------------------ */ + +typedef struct { + FILE *f; +} jpeg_io_t; + +static size_t jpeg_input_cb(JDEC *jd, uint8_t *buf, size_t ndata) +{ + jpeg_io_t *io = (jpeg_io_t *)jd->device; + if (io == NULL || io->f == NULL) { + return 0; + } + if (buf == NULL) { + /* tjpgd requests skipping bytes during header scan */ + if (fseek(io->f, (long)ndata, SEEK_CUR) == 0) { + return ndata; + } + return 0; + } + return fread(buf, 1, ndata, io->f); +} + +static int jpeg_output_cb(JDEC *jd, void *bitmap, JRECT *rect) +{ + (void)jd; + uint16_t *pix = (uint16_t *)bitmap; + int w = rect->right - rect->left + 1; + int h = rect->bottom - rect->top + 1; + + for (int row = 0; row < h; row++) { + /* Convert little-endian RGB565 words (tjpgd) to big-endian wire order */ + for (int col = 0; col < w; col++) { + uint16_t v = pix[row * w + col]; + s_staging[col] = (uint16_t)((v << 8) | (v >> 8)); + } + display_draw_bitmap(rect->left, rect->top + row, rect->left + w, rect->top + row + 1, s_staging); + } + return 1; /* continue decoding */ +} + +esp_err_t display_show_jpeg_file(const char *path) +{ + esp_err_t ret = ESP_FAIL; + FILE *f = fopen(path, "rb"); + if (f == NULL) { + ESP_LOGE(TAG, "cannot open %s", path); + return ESP_ERR_NOT_FOUND; + } + + jpeg_io_t io = { .f = f }; + JDEC jdec; + void *pool = malloc(JPEG_POOL_SIZE); + if (pool == NULL) { + ESP_LOGE(TAG, "jpeg pool alloc failed"); + fclose(f); + return ESP_ERR_NO_MEM; + } + + JRESULT jr = jd_prepare(&jdec, jpeg_input_cb, pool, JPEG_POOL_SIZE, &io); + if (jr != JDR_OK) { + ESP_LOGE(TAG, "jd_prepare failed (%d)", jr); + goto done; + } + + ESP_LOGI(TAG, "decoding %s: %ux%u", path, (unsigned)jdec.width, (unsigned)jdec.height); + jr = jd_decomp(&jdec, jpeg_output_cb, 0); + if (jr != JDR_OK) { + ESP_LOGE(TAG, "jd_decomp failed (%d)", jr); + goto done; + } + ESP_LOGI(TAG, "JPEG displayed: %s", path); + ret = ESP_OK; + +done: + free(pool); + fclose(f); + return ret; +} \ No newline at end of file diff --git a/main/display_manager.h b/main/display_manager.h new file mode 100644 index 0000000..9d5939d --- /dev/null +++ b/main/display_manager.h @@ -0,0 +1,71 @@ +/* + * MTG RFID Companion - ST7789 display manager (Core 1) + * + * Phase 4: Initializes the ST7789 240x320 panel via esp_lcd as a device on + * the shared SPI2_HOST bus, provides RGB565 rendering primitives, glyph text, + * and streaming tjpgd JPEG decode into the panel. + */ + +#pragma once + +#include "esp_err.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define DISP_W (240) +#define DISP_H (320) + +#define RGB565(r, g, b) ((uint16_t)((((r)&0xF8) << 8) | (((g)&0xFC) << 3) | (((b)&0xF8) >> 3))) + +#define COLOR_BLACK RGB565(0, 0, 0) +#define COLOR_WHITE RGB565(255, 255, 255) +#define COLOR_RED RGB565(255, 0, 0) +#define COLOR_GREEN RGB565(0, 255, 0) +#define COLOR_BLUE RGB565(0, 0, 255) +#define COLOR_YELLOW RGB565(255, 255, 0) +#define COLOR_CYAN RGB565(0, 255, 255) +#define COLOR_MAGENTA RGB565(255, 0, 255) +#define COLOR_GRAY RGB565(128, 128, 128) + +/** + * @brief Initialize the ST7789 panel. Requires the shared SPI bus to exist. + */ +esp_err_t display_manager_init(void); + +/** + * @brief Fill the whole screen with a color. + */ +void display_fill(uint16_t color); + +/** + * @brief Fill a rectangle. x1/y1 are exclusive. + */ +void display_fill_rect(int x0, int y0, int x1, int y1, uint16_t color); + +/** + * @brief Draw pixels. x1/y1 exclusive, buffer length = (x1-x0)*(y1-y0) RGB565 words. + */ +void display_draw_bitmap(int x0, int y0, int x1, int y1, const uint16_t *pixels); + +/** + * @brief Draw text with a 5x7 font. Returns pixel width consumed. + */ +int display_put_text(int x, int y, const char *str, uint16_t fg, uint16_t bg); + +/** + * @brief Draw text centered horizontally on the given row. + */ +void display_put_text_center(int y, const char *str, uint16_t fg, uint16_t bg); + +/** + * @brief Stream-decode a JPEG from LittleFS and draw it at the top-left. + * Uses tjpgd with a line-by-line output into the panel DMA path. + */ +esp_err_t display_show_jpeg_file(const char *path); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/main/idf_component.yml b/main/idf_component.yml index 1d8ec2d..73a2042 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -3,4 +3,5 @@ dependencies: idf: ">=5.0" abobija/rc522: "*" - joltwallet/littlefs: "*" \ No newline at end of file + joltwallet/littlefs: "*" + esp_jpeg: ">=1.0.2" \ No newline at end of file diff --git a/main/main.c b/main/main.c index 9a16b58..4f2e9af 100644 --- a/main/main.c +++ b/main/main.c @@ -5,6 +5,8 @@ #include "spi_bus_manager.h" #include "storage_manager.h" #include "rfid_manager.h" +#include "display_manager.h" +#include "ui_task.h" static const char *TAG = "MAIN"; @@ -14,11 +16,14 @@ void app_main(void) ESP_ERROR_CHECK(spi_bus_manager_init()); ESP_ERROR_CHECK(storage_manager_init()); + ESP_ERROR_CHECK(display_manager_init()); ESP_ERROR_CHECK(rfid_manager_init()); ESP_ERROR_CHECK(rfid_manager_start()); - ESP_LOGI(TAG, "Boot sequence complete. Ready for Phase 4+."); + ESP_ERROR_CHECK(ui_task_start()); + + ESP_LOGI(TAG, "Boot sequence complete. Ready for Phase 5+."); while (1) { vTaskDelay(pdMS_TO_TICKS(1000)); diff --git a/main/ui_font.h b/main/ui_font.h new file mode 100644 index 0000000..aedd2d8 --- /dev/null +++ b/main/ui_font.h @@ -0,0 +1,114 @@ +/* + * MTG RFID Companion - 5x7 bitmap font (uppercase, digits, punctuation) + * + * Row-major glyph table. Each glyph is 7 bytes; one byte per pixel row, + * low 5 bits map columns 0..4 from left to right (bit0 = left column). + * Index = ASCII code - 32. + */ + +#pragma once + +#define FONT5X7_W (5) +#define FONT5X7_H (7) +#define FONT5X7_FIRST_CHAR (32) +#define FONT5X7_LAST_CHAR (126) +#define FONT5X7_NUM_CHARS (FONT5X7_LAST_CHAR - FONT5X7_FIRST_CHAR + 1) + +/* (char - 32) -> glyph rows */ +static const uint8_t font5x7[FONT5X7_NUM_CHARS][FONT5X7_H] = { + /* 0x20 ' ' */ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + /* 0x21 '!' */ { 0x04, 0x04, 0x04, 0x04, 0x00, 0x04, 0x04 }, + /* 0x22 '"' */ { 0x0A, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00 }, + /* 0x23 '#' */ { 0x0A, 0x15, 0x1F, 0x15, 0x1F, 0x0A, 0x0A }, + /* 0x24 '$' */ { 0x0E, 0x11, 0x0F, 0x14, 0x0F, 0x11, 0x0E }, + /* 0x25 '%' */ { 0x19, 0x1A, 0x02, 0x04, 0x08, 0x0B, 0x13 }, + /* 0x26 '&' */ { 0x0E, 0x11, 0x09, 0x06, 0x13, 0x11, 0x0E }, + /* 0x27 '\'' */ { 0x06, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00 }, + /* 0x28 '(' */ { 0x02, 0x04, 0x08, 0x08, 0x08, 0x04, 0x02 }, + /* 0x29 ')' */ { 0x08, 0x04, 0x02, 0x02, 0x02, 0x04, 0x08 }, + /* 0x2A '*' */ { 0x04, 0x15, 0x1F, 0x04, 0x04, 0x00, 0x00 }, + /* 0x2B '+' */ { 0x00, 0x04, 0x04, 0x1F, 0x04, 0x04, 0x00 }, + /* 0x2C ',' */ { 0x00, 0x00, 0x00, 0x00, 0x0C, 0x04, 0x08 }, + /* 0x2D '-' */ { 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00 }, + /* 0x2E '.' */ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C }, + /* 0x2F '/' */ { 0x01, 0x01, 0x02, 0x04, 0x08, 0x10, 0x10 }, + /* 0x30 '0' */ { 0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E }, + /* 0x31 '1' */ { 0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x1F }, + /* 0x32 '2' */ { 0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F }, + /* 0x33 '3' */ { 0x0E, 0x01, 0x01, 0x0E, 0x01, 0x01, 0x0E }, + /* 0x34 '4' */ { 0x11, 0x11, 0x11, 0x1F, 0x01, 0x01, 0x01 }, + /* 0x35 '5' */ { 0x1F, 0x10, 0x10, 0x1E, 0x01, 0x01, 0x0E }, + /* 0x36 '6' */ { 0x0E, 0x11, 0x10, 0x1E, 0x11, 0x11, 0x0E }, + /* 0x37 '7' */ { 0x1F, 0x01, 0x02, 0x04, 0x04, 0x04, 0x04 }, + /* 0x38 '8' */ { 0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E }, + /* 0x39 '9' */ { 0x0E, 0x11, 0x11, 0x0F, 0x01, 0x01, 0x0E }, + /* 0x3A ':' */ { 0x00, 0x06, 0x06, 0x00, 0x06, 0x06, 0x00 }, + /* 0x3B ';' */ { 0x00, 0x06, 0x06, 0x00, 0x06, 0x02, 0x04 }, + /* 0x3C '<' */ { 0x02, 0x06, 0x0A, 0x1E, 0x08, 0x08, 0x00 }, + /* 0x3D '=' */ { 0x00, 0x1F, 0x00, 0x1F, 0x00, 0x00, 0x00 }, + /* 0x3E '>' */ { 0x08, 0x0C, 0x0A, 0x0F, 0x02, 0x02, 0x00 }, + /* 0x3F '?' */ { 0x0E, 0x11, 0x01, 0x06, 0x04, 0x00, 0x04 }, + /* 0x40 '@' */ { 0x0E, 0x11, 0x15, 0x1F, 0x10, 0x10, 0x0F }, + /* 0x41 'A' */ { 0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11 }, + /* 0x42 'B' */ { 0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E }, + /* 0x43 'C' */ { 0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E }, + /* 0x44 'D' */ { 0x18, 0x11, 0x11, 0x11, 0x11, 0x11, 0x18 }, + /* 0x45 'E' */ { 0x1F, 0x10, 0x10, 0x1F, 0x10, 0x10, 0x1F }, + /* 0x46 'F' */ { 0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10 }, + /* 0x47 'G' */ { 0x0E, 0x11, 0x10, 0x13, 0x11, 0x11, 0x0F }, + /* 0x48 'H' */ { 0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11 }, + /* 0x49 'I' */ { 0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x1F }, + /* 0x4A 'J' */ { 0x01, 0x01, 0x01, 0x01, 0x11, 0x11, 0x0E }, + /* 0x4B 'K' */ { 0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11 }, + /* 0x4C 'L' */ { 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F }, + /* 0x4D 'M' */ { 0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x11 }, + /* 0x4E 'N' */ { 0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x11 }, + /* 0x4F 'O' */ { 0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E }, + /* 0x50 'P' */ { 0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10 }, + /* 0x51 'Q' */ { 0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D }, + /* 0x52 'R' */ { 0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11 }, + /* 0x53 'S' */ { 0x0E, 0x11, 0x10, 0x0E, 0x01, 0x11, 0x0E }, + /* 0x54 'T' */ { 0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04 }, + /* 0x55 'U' */ { 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E }, + /* 0x56 'V' */ { 0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04 }, + /* 0x57 'W' */ { 0x11, 0x11, 0x11, 0x13, 0x15, 0x19, 0x11 }, + /* 0x58 'X' */ { 0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11 }, + /* 0x59 'Y' */ { 0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04 }, + /* 0x5A 'Z' */ { 0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F }, + /* 0x5B '[' */ { 0x08, 0x0C, 0x08, 0x08, 0x08, 0x08, 0x0E }, + /* 0x5C '\\' */ { 0x10, 0x10, 0x08, 0x04, 0x02, 0x01, 0x01 }, + /* 0x5D ']' */ { 0x0E, 0x04, 0x04, 0x04, 0x04, 0x0C, 0x04 }, + /* 0x5E '^' */ { 0x04, 0x0A, 0x15, 0x04, 0x04, 0x00, 0x00 }, + /* 0x5F '_' */ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F }, + /* 0x60 '`' */ { 0x06, 0x08, 0x04, 0x00, 0x00, 0x00, 0x00 }, + /* 0x61 'a' */ { 0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11 }, + /* 0x62 'b' */ { 0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E }, + /* 0x63 'c' */ { 0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E }, + /* 0x64 'd' */ { 0x18, 0x11, 0x11, 0x11, 0x11, 0x11, 0x18 }, + /* 0x65 'e' */ { 0x1F, 0x10, 0x10, 0x1F, 0x10, 0x10, 0x1F }, + /* 0x66 'f' */ { 0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10 }, + /* 0x67 'g' */ { 0x0E, 0x11, 0x10, 0x13, 0x11, 0x11, 0x0F }, + /* 0x68 'h' */ { 0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11 }, + /* 0x69 'i' */ { 0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x1F }, + /* 0x6A 'j' */ { 0x01, 0x01, 0x01, 0x01, 0x11, 0x11, 0x0E }, + /* 0x6B 'k' */ { 0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11 }, + /* 0x6C 'l' */ { 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F }, + /* 0x6D 'm' */ { 0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x11 }, + /* 0x6E 'n' */ { 0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x11 }, + /* 0x6F 'o' */ { 0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E }, + /* 0x70 'p' */ { 0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10 }, + /* 0x71 'q' */ { 0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D }, + /* 0x72 'r' */ { 0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11 }, + /* 0x73 's' */ { 0x0E, 0x11, 0x10, 0x0E, 0x01, 0x11, 0x0E }, + /* 0x74 't' */ { 0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04 }, + /* 0x75 'u' */ { 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E }, + /* 0x76 'v' */ { 0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04 }, + /* 0x77 'w' */ { 0x11, 0x11, 0x11, 0x13, 0x15, 0x19, 0x11 }, + /* 0x78 'x' */ { 0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11 }, + /* 0x79 'y' */ { 0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04 }, + /* 0x7A 'z' */ { 0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F }, + /* 0x7B '{' */ { 0x06, 0x04, 0x08, 0x0C, 0x08, 0x04, 0x18 }, + /* 0x7C '|' */ { 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04 }, + /* 0x7D '}' */ { 0x0C, 0x04, 0x02, 0x06, 0x02, 0x04, 0x06 }, + /* 0x7E '~' */ { 0x00, 0x1B, 0x16, 0x00, 0x00, 0x00, 0x00 }, +}; \ No newline at end of file diff --git a/main/ui_task.c b/main/ui_task.c new file mode 100644 index 0000000..9c997fd --- /dev/null +++ b/main/ui_task.c @@ -0,0 +1,75 @@ +/* + * MTG RFID Companion - UI task (Core 1) + */ + +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include "display_manager.h" +#include "ui_task.h" + +static const char *TAG = "ui"; + +#define UI_TASK_STACK (8 * 1024) +#define UI_TASK_PRIO (5) +#define UI_CORE (1) + +static TaskHandle_t s_ui_task = NULL; + +static void draw_color_bars(void) +{ + static const uint16_t bars[] = { + COLOR_WHITE, COLOR_YELLOW, COLOR_CYAN, COLOR_GREEN, + COLOR_MAGENTA, COLOR_RED, COLOR_BLUE, COLOR_BLACK + }; + const int band = DISP_W / 8; + for (int i = 0; i < 8; i++) { + display_fill_rect(i * band, 0, (i + 1) * band, 64, bars[i]); + } +} + +static void ui_boot_demo(void) +{ + ESP_LOGI(TAG, "UI demo: color bars"); + draw_color_bars(); + vTaskDelay(pdMS_TO_TICKS(1500)); + + ESP_LOGI(TAG, "UI demo: text rendering"); + display_fill(COLOR_BLACK); + display_put_text_center(8, "MTG RFID COMPANION", COLOR_YELLOW, COLOR_BLACK); + display_put_text(16, 40, "UID: 04A2B3C4", COLOR_GREEN, COLOR_BLACK); + display_put_text(16, 56, "SOL RING", COLOR_WHITE, COLOR_BLACK); + display_put_text(16, 72, "MANA {1}", COLOR_CYAN, COLOR_BLACK); + display_put_text(16, 88, "PRICE $1.25", COLOR_MAGENTA, COLOR_BLACK); + vTaskDelay(pdMS_TO_TICKS(2000)); + + ESP_LOGI(TAG, "UI demo: JPEG decode from LittleFS"); + display_show_jpeg_file("/spiffs/cards/sample.jpg"); + vTaskDelay(pdMS_TO_TICKS(3000)); + + ESP_LOGI(TAG, "UI demo complete"); +} + +static void ui_task(void *arg) +{ + (void)arg; + ESP_LOGI(TAG, "UI task on core %d", xPortGetCoreID()); + ui_boot_demo(); + ESP_LOGI(TAG, "UI task entering idle render loop"); + while (1) { + /* Placeholder idle loop; state-driven rendering lands in Phase 7. */ + vTaskDelay(pdMS_TO_TICKS(100)); + } +} + +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) +{ + /* Expose for later phases; currently no-op before state machine lands. */ +} \ No newline at end of file diff --git a/main/ui_task.h b/main/ui_task.h new file mode 100644 index 0000000..23cff8a --- /dev/null +++ b/main/ui_task.h @@ -0,0 +1,29 @@ +/* + * MTG RFID Companion - UI task (Core 1) + * + * Phase 4: Dedicated renderer task pinned to Core 1. Runs the boot-time + * visual validation sequence (color bars, text, JPEG decode). + */ + +#pragma once + +#include "esp_err.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Create and start the UI renderer task pinned to Core 1. + */ +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/sdkconfig b/sdkconfig index 82e5275..0d29b75 100644 --- a/sdkconfig +++ b/sdkconfig @@ -2053,6 +2053,23 @@ CONFIG_WIFI_PROV_STA_ALL_CHANNEL_SCAN=y CONFIG_RC522_PREVENT_SECTOR_TRAILER_WRITE=y # end of RC522 +# +# JPEG Decoder +# +# CONFIG_JD_USE_ROM is not set +CONFIG_JD_SZBUF=512 +CONFIG_JD_FORMAT=1 +# CONFIG_JD_FORMAT_RGB888 is not set +CONFIG_JD_FORMAT_RGB565=y +CONFIG_JD_USE_SCALE=y +CONFIG_JD_TBLCLIP=y +CONFIG_JD_FASTDECODE=1 +# CONFIG_JD_FASTDECODE_BASIC is not set +CONFIG_JD_FASTDECODE_32BIT=y +# CONFIG_JD_FASTDECODE_TABLE is not set +# CONFIG_JD_DEFAULT_HUFFMAN is not set +# end of JPEG Decoder + # # LittleFS # diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 1233692..b03d2a7 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -5,4 +5,10 @@ CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" CONFIG_ESPTOOLPY_FLASHMODE_DIO=y CONFIG_VFS_SUPPORT_DIR=y -CONFIG_FREERTOS_SMP=y \ No newline at end of file +CONFIG_FREERTOS_SMP=y + +# tjpgd / esp_jpeg: use source tjpgd (granular line-by-line API), RGB565 out +CONFIG_JD_USE_ROM=n +CONFIG_JD_SZBUF=512 +CONFIG_JD_FORMAT_RGB565=y +CONFIG_JD_FASTDECODE_32BIT=y \ No newline at end of file diff --git a/storage/cards/sample.jpg b/storage/cards/sample.jpg new file mode 100644 index 0000000000000000000000000000000000000000..251af3d4cd6951e10b9f3e08b4071a2587a6c974 GIT binary patch literal 23530 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 H|8D{SE?;j- literal 0 HcmV?d00001 diff --git a/storage/cards/sample.json b/storage/cards/sample.json new file mode 100644 index 0000000..85cde57 --- /dev/null +++ b/storage/cards/sample.json @@ -0,0 +1,7 @@ +{ + "name": "Sol Ring", + "mana_cost": "{1}", + "type": "Artifact", + "oracle": "Tap: Add CC.", + "price_usd": "1.25" +} \ No newline at end of file diff --git a/storage/mappings.json b/storage/mappings.json new file mode 100644 index 0000000..27d928a --- /dev/null +++ b/storage/mappings.json @@ -0,0 +1,4 @@ +{ + "04A2B3C4": "Sol Ring", + "04E1F2A3": "Atraxa, Praetors' Voice" +} \ No newline at end of file diff --git a/tools/make_sample_jpg.ps1 b/tools/make_sample_jpg.ps1 new file mode 100644 index 0000000..af0bb27 --- /dev/null +++ b/tools/make_sample_jpg.ps1 @@ -0,0 +1,56 @@ +$ErrorActionPreference = "Stop" +Add-Type -AssemblyName System.Drawing + +$dir = "storage\cards" +New-Item -ItemType Directory -Force -Path $dir | Out-Null +New-Item -ItemType Directory -Force -Path "storage" | Out-Null + +$w = 240 +$h = 320 +$bmp = New-Object System.Drawing.Bitmap($w, $h) +$g = [System.Drawing.Graphics]::FromImage($bmp) +$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias + +# Vertical gradient: one fast FillRectangle per row +for ($y = 0; $y -lt $h; $y++) { + $blue = [byte]([int]((255 * $y) / ($h - 1))) + $green = [byte]([int]((255 * (($y * 7) % $h)) / $h)) + $red = [byte]([int]((255 * (($y * 3) % $h)) / $h)) + $b = [System.Drawing.SolidBrush]::new([System.Drawing.Color]::FromArgb($red, $green, $blue)) + $g.FillRectangle($b, 0, $y, $w, 1) + $b.Dispose() +} + +# Diagonal stripe for visual aliasing check +$stripe = [System.Drawing.SolidBrush]::new([System.Drawing.Color]::FromArgb(255, 255, 255)) +for ($x = -$h; $x -lt $w; $x += 40) { + $pts = [System.Drawing.Point[]]@( + [System.Drawing.Point]::new($x, 0), + [System.Drawing.Point]::new($x + 20, 0), + [System.Drawing.Point]::new($x + 20 + $h, $h), + [System.Drawing.Point]::new($x + $h, $h) + ) + $g.FillPolygon($stripe, $pts) +} +$stripe.Dispose() + +$font = [System.Drawing.Font]::new([System.Drawing.FontFamily]::GenericSansSerif, 22, [System.Drawing.FontStyle]::Bold) +$font2 = [System.Drawing.Font]::new([System.Drawing.FontFamily]::GenericSansSerif, 14, [System.Drawing.FontStyle]::Bold) +$white = [System.Drawing.Brushes]::White +$black = [System.Drawing.Brushes]::Black + +# Text with outline +$txt1 = "MTG RFID" +$txt2 = "COMPANION" +$sz1 = $g.MeasureString($txt1, $font) +$g.DrawString($txt1, $font, $black, 28 - 2, 40 - 2) +$g.DrawString($txt1, $font, $white, 28, 40) +$g.DrawString($txt2, $font, $black, 28 - 2, 40 + $sz1.Height - 2) +$g.DrawString($txt2, $font, $white, 28, 40 + $sz1.Height) +$g.DrawString("240x320 RGB565", $font2, $white, 28, 260) +$g.DrawString("ST7789 TJPGD", $font2, $white, 28, 285) + +$bmp.Save("$dir\sample.jpg", [System.Drawing.Imaging.ImageFormat]::Jpeg) +$g.Dispose() +$bmp.Dispose() +Get-Item "$dir\sample.jpg" | Select-Object FullName, Length \ No newline at end of file