Phase 4: ST7789 240x320 panel (esp_lcd), 5x7 font, Core 1 UI task, streaming tjpgd JPEG decode + baked storage image

This commit is contained in:
2026-09-09 17:35:54 +02:00
parent ec1e7a0d5a
commit e09ec04946
15 changed files with 631 additions and 4 deletions

View File

@@ -4,4 +4,7 @@ include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(mtg-rfid-companion) project(mtg-rfid-companion)
idf_build_set_property(PROJECT_DESCRIPTION "MTG RFID Companion") idf_build_set_property(PROJECT_DESCRIPTION "MTG RFID Companion")
idf_build_set_property(PROJECT_HOMEPAGE_URL "https://scryfall.com") 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)

View File

@@ -3,5 +3,7 @@ idf_component_register(SRCS
"spi_bus_manager.c" "spi_bus_manager.c"
"storage_manager.c" "storage_manager.c"
"rfid_manager.c" "rfid_manager.c"
"display_manager.c"
"ui_task.c"
INCLUDE_DIRS "." INCLUDE_DIRS "."
) )

237
main/display_manager.c Normal file
View File

@@ -0,0 +1,237 @@
/*
* MTG RFID Companion - ST7789 display manager (Core 1)
*/
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#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;
}

71
main/display_manager.h Normal file
View File

@@ -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 <stdint.h>
#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

View File

@@ -3,4 +3,5 @@
dependencies: dependencies:
idf: ">=5.0" idf: ">=5.0"
abobija/rc522: "*" abobija/rc522: "*"
joltwallet/littlefs: "*" joltwallet/littlefs: "*"
esp_jpeg: ">=1.0.2"

View File

@@ -5,6 +5,8 @@
#include "spi_bus_manager.h" #include "spi_bus_manager.h"
#include "storage_manager.h" #include "storage_manager.h"
#include "rfid_manager.h" #include "rfid_manager.h"
#include "display_manager.h"
#include "ui_task.h"
static const char *TAG = "MAIN"; static const char *TAG = "MAIN";
@@ -14,11 +16,14 @@ void app_main(void)
ESP_ERROR_CHECK(spi_bus_manager_init()); ESP_ERROR_CHECK(spi_bus_manager_init());
ESP_ERROR_CHECK(storage_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_init());
ESP_ERROR_CHECK(rfid_manager_start()); 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) { while (1) {
vTaskDelay(pdMS_TO_TICKS(1000)); vTaskDelay(pdMS_TO_TICKS(1000));

114
main/ui_font.h Normal file
View File

@@ -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 },
};

75
main/ui_task.c Normal file
View File

@@ -0,0 +1,75 @@
/*
* MTG RFID Companion - UI task (Core 1)
*/
#include <string.h>
#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. */
}

29
main/ui_task.h Normal file
View File

@@ -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 <stdbool.h>
#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

View File

@@ -2053,6 +2053,23 @@ CONFIG_WIFI_PROV_STA_ALL_CHANNEL_SCAN=y
CONFIG_RC522_PREVENT_SECTOR_TRAILER_WRITE=y CONFIG_RC522_PREVENT_SECTOR_TRAILER_WRITE=y
# end of RC522 # 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 # LittleFS
# #

View File

@@ -5,4 +5,10 @@ CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_ESPTOOLPY_FLASHMODE_DIO=y CONFIG_ESPTOOLPY_FLASHMODE_DIO=y
CONFIG_VFS_SUPPORT_DIR=y CONFIG_VFS_SUPPORT_DIR=y
CONFIG_FREERTOS_SMP=y 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

BIN
storage/cards/sample.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,7 @@
{
"name": "Sol Ring",
"mana_cost": "{1}",
"type": "Artifact",
"oracle": "Tap: Add CC.",
"price_usd": "1.25"
}

4
storage/mappings.json Normal file
View File

@@ -0,0 +1,4 @@
{
"04A2B3C4": "Sol Ring",
"04E1F2A3": "Atraxa, Praetors' Voice"
}

56
tools/make_sample_jpg.ps1 Normal file
View File

@@ -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