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 0000000..251af3d Binary files /dev/null and b/storage/cards/sample.jpg differ 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